mediawiki.api: Adopt async-await and assert.rejects() in various tests
[mediawiki.git] / resources / lib / jquery.i18n / src / jquery.i18n.js
blobd6c0bae611dddb81e1adb17447840280b1d29ff2
1 /*!
2  * jQuery Internationalization library
3  *
4  * Copyright (C) 2012 Santhosh Thottingal
5  *
6  * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do
7  * anything special to choose one license or the other and you don't have to
8  * notify anyone which license you are using. You are free to use
9  * UniversalLanguageSelector in commercial projects as long as the copyright
10  * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
11  *
12  * @licence GNU General Public Licence 2.0 or later
13  * @licence MIT License
14  */
16 ( function ( $ ) {
17         'use strict';
19         var I18N,
20                 slice = Array.prototype.slice;
21         /**
22          * @constructor
23          * @param {Object} options
24          */
25         I18N = function ( options ) {
26                 // Load defaults
27                 this.options = $.extend( {}, I18N.defaults, options );
29                 this.parser = this.options.parser;
30                 this.locale = this.options.locale;
31                 this.messageStore = this.options.messageStore;
32                 this.languages = {};
33         };
35         I18N.prototype = {
36                 /**
37                  * Localize a given messageKey to a locale.
38                  * @param {string} messageKey
39                  * @return {string} Localized message
40                  */
41                 localize: function ( messageKey ) {
42                         var localeParts, localePartIndex, locale, fallbackIndex,
43                                 tryingLocale, message;
45                         locale = this.locale;
46                         fallbackIndex = 0;
48                         while ( locale ) {
49                                 // Iterate through locales starting at most-specific until
50                                 // localization is found. As in fi-Latn-FI, fi-Latn and fi.
51                                 localeParts = locale.split( '-' );
52                                 localePartIndex = localeParts.length;
54                                 do {
55                                         tryingLocale = localeParts.slice( 0, localePartIndex ).join( '-' );
56                                         message = this.messageStore.get( tryingLocale, messageKey );
58                                         if ( message ) {
59                                                 return message;
60                                         }
62                                         localePartIndex--;
63                                 } while ( localePartIndex );
65                                 if ( locale === this.options.fallbackLocale ) {
66                                         break;
67                                 }
69                                 locale = ( $.i18n.fallbacks[ this.locale ] &&
70                                                 $.i18n.fallbacks[ this.locale ][ fallbackIndex ] ) ||
71                                                 this.options.fallbackLocale;
72                                 $.i18n.log( 'Trying fallback locale for ' + this.locale + ': ' + locale + ' (' + messageKey + ')' );
74                                 fallbackIndex++;
75                         }
77                         // key not found
78                         return '';
79                 },
81                 /*
82                  * Destroy the i18n instance.
83                  */
84                 destroy: function () {
85                         $.removeData( document, 'i18n' );
86                 },
88                 /**
89                  * General message loading API This can take a URL string for
90                  * the json formatted messages. Example:
91                  * <code>load('path/to/all_localizations.json');</code>
92                  *
93                  * To load a localization file for a locale:
94                  * <code>
95                  * load('path/to/de-messages.json', 'de' );
96                  * </code>
97                  *
98                  * To load a localization file from a directory:
99                  * <code>
100                  * load('path/to/i18n/directory', 'de' );
101                  * </code>
102                  * The above method has the advantage of fallback resolution.
103                  * ie, it will automatically load the fallback locales for de.
104                  * For most usecases, this is the recommended method.
105                  * It is optional to have trailing slash at end.
106                  *
107                  * A data object containing message key- message translation mappings
108                  * can also be passed. Example:
109                  * <code>
110                  * load( { 'hello' : 'Hello' }, optionalLocale );
111                  * </code>
112                  *
113                  * A source map containing key-value pair of languagename and locations
114                  * can also be passed. Example:
115                  * <code>
116                  * load( {
117                  * bn: 'i18n/bn.json',
118                  * he: 'i18n/he.json',
119                  * en: 'i18n/en.json'
120                  * } )
121                  * </code>
122                  *
123                  * If the data argument is null/undefined/false,
124                  * all cached messages for the i18n instance will get reset.
125                  *
126                  * @param {string|Object} source
127                  * @param {string} locale Language tag
128                  * @return {jQuery.Promise}
129                  */
130                 load: function ( source, locale ) {
131                         var fallbackLocales, locIndex, fallbackLocale, sourceMap = {};
132                         if ( !source && !locale ) {
133                                 source = 'i18n/' + $.i18n().locale + '.json';
134                                 locale = $.i18n().locale;
135                         }
136                         if ( typeof source === 'string' &&
137                                 // source extension should be json, but can have query params after that.
138                                 source.split( '?' )[ 0 ].split( '.' ).pop() !== 'json'
139                         ) {
140                                 // Load specified locale then check for fallbacks when directory is
141                                 // specified in load()
142                                 sourceMap[ locale ] = source + '/' + locale + '.json';
143                                 fallbackLocales = ( $.i18n.fallbacks[ locale ] || [] )
144                                         .concat( this.options.fallbackLocale );
145                                 for ( locIndex = 0; locIndex < fallbackLocales.length; locIndex++ ) {
146                                         fallbackLocale = fallbackLocales[ locIndex ];
147                                         sourceMap[ fallbackLocale ] = source + '/' + fallbackLocale + '.json';
148                                 }
149                                 return this.load( sourceMap );
150                         } else {
151                                 return this.messageStore.load( source, locale );
152                         }
154                 },
156                 /**
157                  * Does parameter and magic word substitution.
158                  *
159                  * @param {string} key Message key
160                  * @param {Array} parameters Message parameters
161                  * @return {string}
162                  */
163                 parse: function ( key, parameters ) {
164                         var message = this.localize( key );
165                         // FIXME: This changes the state of the I18N object,
166                         // should probably not change the 'this.parser' but just
167                         // pass it to the parser.
168                         this.parser.language = $.i18n.languages[ $.i18n().locale ] || $.i18n.languages[ 'default' ];
169                         if ( message === '' ) {
170                                 message = key;
171                         }
172                         return this.parser.parse( message, parameters );
173                 }
174         };
176         /**
177          * Process a message from the $.I18N instance
178          * for the current document, stored in jQuery.data(document).
179          *
180          * @param {string} key Key of the message.
181          * @param {string} param1 [param...] Variadic list of parameters for {key}.
182          * @return {string|$.I18N} Parsed message, or if no key was given
183          * the instance of $.I18N is returned.
184          */
185         $.i18n = function ( key, param1 ) {
186                 var parameters,
187                         i18n = $.data( document, 'i18n' ),
188                         options = typeof key === 'object' && key;
190                 // If the locale option for this call is different then the setup so far,
191                 // update it automatically. This doesn't just change the context for this
192                 // call but for all future call as well.
193                 // If there is no i18n setup yet, don't do this. It will be taken care of
194                 // by the `new I18N` construction below.
195                 // NOTE: It should only change language for this one call.
196                 // Then cache instances of I18N somewhere.
197                 if ( options && options.locale && i18n && i18n.locale !== options.locale ) {
198                         i18n.locale = options.locale;
199                 }
201                 if ( !i18n ) {
202                         i18n = new I18N( options );
203                         $.data( document, 'i18n', i18n );
204                 }
206                 if ( typeof key === 'string' ) {
207                         if ( param1 !== undefined ) {
208                                 parameters = slice.call( arguments, 1 );
209                         } else {
210                                 parameters = [];
211                         }
213                         return i18n.parse( key, parameters );
214                 } else {
215                         // FIXME: remove this feature/bug.
216                         return i18n;
217                 }
218         };
220         $.fn.i18n = function () {
221                 var i18n = $.data( document, 'i18n' );
223                 if ( !i18n ) {
224                         i18n = new I18N();
225                         $.data( document, 'i18n', i18n );
226                 }
228                 return this.each( function () {
229                         var $this = $( this ),
230                                 messageKey = $this.data( 'i18n' ),
231                                 lBracket, rBracket, type, key;
233                         if ( messageKey ) {
234                                 lBracket = messageKey.indexOf( '[' );
235                                 rBracket = messageKey.indexOf( ']' );
236                                 if ( lBracket !== -1 && rBracket !== -1 && lBracket < rBracket ) {
237                                         type = messageKey.slice( lBracket + 1, rBracket );
238                                         key = messageKey.slice( rBracket + 1 );
239                                         if ( type === 'html' ) {
240                                                 $this.html( i18n.parse( key ) );
241                                         } else {
242                                                 $this.attr( type, i18n.parse( key ) );
243                                         }
244                                 } else {
245                                         $this.text( i18n.parse( messageKey ) );
246                                 }
247                         } else {
248                                 $this.find( '[data-i18n]' ).i18n();
249                         }
250                 } );
251         };
253         function getDefaultLocale() {
254                 var locale = $( 'html' ).attr( 'lang' );
255                 if ( !locale ) {
256                         locale = navigator.language || navigator.userLanguage || '';
257                 }
258                 return locale;
259         }
261         $.i18n.languages = {};
262         $.i18n.messageStore = $.i18n.messageStore || {};
263         $.i18n.parser = {
264                 // The default parser only handles variable substitution
265                 parse: function ( message, parameters ) {
266                         return message.replace( /\$(\d+)/g, function ( str, match ) {
267                                 var index = parseInt( match, 10 ) - 1;
268                                 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
269                         } );
270                 },
271                 emitter: {}
272         };
273         $.i18n.fallbacks = {};
274         $.i18n.debug = false;
275         $.i18n.log = function ( /* arguments */ ) {
276                 if ( window.console && $.i18n.debug ) {
277                         window.console.log.apply( window.console, arguments );
278                 }
279         };
280         /* Static members */
281         I18N.defaults = {
282                 locale: getDefaultLocale(),
283                 fallbackLocale: 'en',
284                 parser: $.i18n.parser,
285                 messageStore: $.i18n.messageStore
286         };
288         // Expose constructor
289         $.i18n.constructor = I18N;
290 }( jQuery ) );