Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / resources / lib / jquery.i18n / src / jquery.i18n.js
blob9236e4e2b542653851ac6215581c694fe1def98a
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 nav, 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 = {};
34                 this.init();
35         };
37         I18N.prototype = {
38                 /**
39                  * Initialize by loading locales and setting up
40                  * String.prototype.toLocaleString and String.locale.
41                  */
42                 init: function () {
43                         var i18n = this;
45                         // Set locale of String environment
46                         String.locale = i18n.locale;
48                         // Override String.localeString method
49                         String.prototype.toLocaleString = function () {
50                                 var localeParts, localePartIndex, value, locale, fallbackIndex,
51                                         tryingLocale, message;
53                                 value = this.valueOf();
54                                 locale = i18n.locale;
55                                 fallbackIndex = 0;
57                                 while ( locale ) {
58                                         // Iterate through locales starting at most-specific until
59                                         // localization is found. As in fi-Latn-FI, fi-Latn and fi.
60                                         localeParts = locale.split( '-' );
61                                         localePartIndex = localeParts.length;
63                                         do {
64                                                 tryingLocale = localeParts.slice( 0, localePartIndex ).join( '-' );
65                                                 message = i18n.messageStore.get( tryingLocale, value );
67                                                 if ( message ) {
68                                                         return message;
69                                                 }
71                                                 localePartIndex--;
72                                         } while ( localePartIndex );
74                                         if ( locale === 'en' ) {
75                                                 break;
76                                         }
78                                         locale = ( $.i18n.fallbacks[i18n.locale] && $.i18n.fallbacks[i18n.locale][fallbackIndex] ) ||
79                                                 i18n.options.fallbackLocale;
80                                         $.i18n.log( 'Trying fallback locale for ' + i18n.locale + ': ' + locale );
82                                         fallbackIndex++;
83                                 }
85                                 // key not found
86                                 return '';
87                         };
88                 },
90                 /*
91                  * Destroy the i18n instance.
92                  */
93                 destroy: function () {
94                         $.removeData( document, 'i18n' );
95                 },
97                 /**
98                  * General message loading API This can take a URL string for
99                  * the json formatted messages. Example:
100                  * <code>load('path/to/all_localizations.json');</code>
101                  *
102                  * To load a localization file for a locale:
103                  * <code>
104                  * load('path/to/de-messages.json', 'de' );
105                  * </code>
106                  *
107                  * To load a localization file from a directory:
108                  * <code>
109                  * load('path/to/i18n/directory', 'de' );
110                  * </code>
111                  * The above method has the advantage of fallback resolution.
112                  * ie, it will automatically load the fallback locales for de.
113                  * For most usecases, this is the recommended method.
114                  * It is optional to have trailing slash at end.
115                  *
116                  * A data object containing message key- message translation mappings
117                  * can also be passed. Example:
118                  * <code>
119                  * load( { 'hello' : 'Hello' }, optionalLocale );
120                  * </code>
121                  *
122                  * A source map containing key-value pair of languagename and locations
123                  * can also be passed. Example:
124                  * <code>
125                  * load( {
126                  * bn: 'i18n/bn.json',
127                  * he: 'i18n/he.json',
128                  * en: 'i18n/en.json'
129                  * } )
130                  * </code>
131                  *
132                  * If the data argument is null/undefined/false,
133                  * all cached messages for the i18n instance will get reset.
134                  *
135                  * @param {String|Object} source
136                  * @param {String} locale Language tag
137                  * @returns {jQuery.Promise}
138                  */
139                 load: function ( source, locale ) {
140                         var fallbackLocales, locIndex, fallbackLocale, sourceMap = {};
141                         if ( !source && !locale ) {
142                                 source = 'i18n/' + $.i18n().locale + '.json';
143                                 locale = $.i18n().locale;
144                         }
145                         if ( typeof source === 'string' &&
146                                 source.split( '.' ).pop() !== 'json'
147                         ) {
148                                 // Load specified locale then check for fallbacks when directory is specified in load()
149                                 sourceMap[locale] = source + '/' + locale + '.json';
150                                 fallbackLocales = ( $.i18n.fallbacks[locale] || [] )
151                                         .concat( this.options.fallbackLocale );
152                                 for ( locIndex in fallbackLocales ) {
153                                         fallbackLocale = fallbackLocales[locIndex];
154                                         sourceMap[fallbackLocale] = source + '/' + fallbackLocale + '.json';
155                                 }
156                                 return this.load( sourceMap );
157                         } else {
158                                 return this.messageStore.load( source, locale );
159                         }
161                 },
163                 /**
164                  * Does parameter and magic word substitution.
165                  *
166                  * @param {string} key Message key
167                  * @param {Array} parameters Message parameters
168                  * @return {string}
169                  */
170                 parse: function ( key, parameters ) {
171                         var message = key.toLocaleString();
172                         // FIXME: This changes the state of the I18N object,
173                         // should probably not change the 'this.parser' but just
174                         // pass it to the parser.
175                         this.parser.language = $.i18n.languages[$.i18n().locale] || $.i18n.languages['default'];
176                         if ( message === '' ) {
177                                 message = key;
178                         }
179                         return this.parser.parse( message, parameters );
180                 }
181         };
183         /**
184          * Process a message from the $.I18N instance
185          * for the current document, stored in jQuery.data(document).
186          *
187          * @param {string} key Key of the message.
188          * @param {string} param1 [param...] Variadic list of parameters for {key}.
189          * @return {string|$.I18N} Parsed message, or if no key was given
190          * the instance of $.I18N is returned.
191          */
192         $.i18n = function ( key, param1 ) {
193                 var parameters,
194                         i18n = $.data( document, 'i18n' ),
195                         options = typeof key === 'object' && key;
197                 // If the locale option for this call is different then the setup so far,
198                 // update it automatically. This doesn't just change the context for this
199                 // call but for all future call as well.
200                 // If there is no i18n setup yet, don't do this. It will be taken care of
201                 // by the `new I18N` construction below.
202                 // NOTE: It should only change language for this one call.
203                 // Then cache instances of I18N somewhere.
204                 if ( options && options.locale && i18n && i18n.locale !== options.locale ) {
205                         String.locale = i18n.locale = options.locale;
206                 }
208                 if ( !i18n ) {
209                         i18n = new I18N( options );
210                         $.data( document, 'i18n', i18n );
211                 }
213                 if ( typeof key === 'string' ) {
214                         if ( param1 !== undefined ) {
215                                 parameters = slice.call( arguments, 1 );
216                         } else {
217                                 parameters = [];
218                         }
220                         return i18n.parse( key, parameters );
221                 } else {
222                         // FIXME: remove this feature/bug.
223                         return i18n;
224                 }
225         };
227         $.fn.i18n = function () {
228                 var i18n = $.data( document, 'i18n' );
230                 if ( !i18n ) {
231                         i18n = new I18N();
232                         $.data( document, 'i18n', i18n );
233                 }
234                 String.locale = i18n.locale;
235                 return this.each( function () {
236                         var $this = $( this ),
237                                 messageKey = $this.data( 'i18n' );
239                         if ( messageKey ) {
240                                 $this.text( i18n.parse( messageKey ) );
241                         } else {
242                                 $this.find( '[data-i18n]' ).i18n();
243                         }
244                 } );
245         };
247         String.locale = String.locale || $( 'html' ).attr( 'lang' );
249         if ( !String.locale ) {
250                 if ( typeof window.navigator !== undefined ) {
251                         nav = window.navigator;
252                         String.locale = nav.language || nav.userLanguage || '';
253                 } else {
254                         String.locale = '';
255                 }
256         }
258         $.i18n.languages = {};
259         $.i18n.messageStore = $.i18n.messageStore || {};
260         $.i18n.parser = {
261                 // The default parser only handles variable substitution
262                 parse: function ( message, parameters ) {
263                         return message.replace( /\$(\d+)/g, function ( str, match ) {
264                                 var index = parseInt( match, 10 ) - 1;
265                                 return parameters[index] !== undefined ? parameters[index] : '$' + match;
266                         } );
267                 },
268                 emitter: {}
269         };
270         $.i18n.fallbacks = {};
271         $.i18n.debug = false;
272         $.i18n.log = function ( /* arguments */ ) {
273                 if ( window.console && $.i18n.debug ) {
274                         window.console.log.apply( window.console, arguments );
275                 }
276         };
277         /* Static members */
278         I18N.defaults = {
279                 locale: String.locale,
280                 fallbackLocale: 'en',
281                 parser: $.i18n.parser,
282                 messageStore: $.i18n.messageStore
283         };
285         // Expose constructor
286         $.i18n.constructor = I18N;
287 }( jQuery ) );