wikimedia/mediawiki-core

View on GitHub
resources/lib/jquery.i18n/src/jquery.i18n.js

Summary

Maintainability
A
3 hrs
Test Coverage
/*!
 * jQuery Internationalization library
 *
 * Copyright (C) 2012 Santhosh Thottingal
 *
 * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do
 * anything special to choose one license or the other and you don't have to
 * notify anyone which license you are using. You are free to use
 * UniversalLanguageSelector in commercial projects as long as the copyright
 * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
 *
 * @licence GNU General Public Licence 2.0 or later
 * @licence MIT License
 */

( function ( $ ) {
    'use strict';

    var I18N,
        slice = Array.prototype.slice;
    /**
     * @constructor
     * @param {Object} options
     */
    I18N = function ( options ) {
        // Load defaults
        this.options = $.extend( {}, I18N.defaults, options );

        this.parser = this.options.parser;
        this.locale = this.options.locale;
        this.messageStore = this.options.messageStore;
        this.languages = {};
    };

    I18N.prototype = {
        /**
         * Localize a given messageKey to a locale.
         * @param {string} messageKey
         * @return {string} Localized message
         */
        localize: function ( messageKey ) {
            var localeParts, localePartIndex, locale, fallbackIndex,
                tryingLocale, message;

            locale = this.locale;
            fallbackIndex = 0;

            while ( locale ) {
                // Iterate through locales starting at most-specific until
                // localization is found. As in fi-Latn-FI, fi-Latn and fi.
                localeParts = locale.split( '-' );
                localePartIndex = localeParts.length;

                do {
                    tryingLocale = localeParts.slice( 0, localePartIndex ).join( '-' );
                    message = this.messageStore.get( tryingLocale, messageKey );

                    if ( message ) {
                        return message;
                    }

                    localePartIndex--;
                } while ( localePartIndex );

                if ( locale === this.options.fallbackLocale ) {
                    break;
                }

                locale = ( $.i18n.fallbacks[ this.locale ] &&
                        $.i18n.fallbacks[ this.locale ][ fallbackIndex ] ) ||
                        this.options.fallbackLocale;
                $.i18n.log( 'Trying fallback locale for ' + this.locale + ': ' + locale + ' (' + messageKey + ')' );

                fallbackIndex++;
            }

            // key not found
            return '';
        },

        /*
         * Destroy the i18n instance.
         */
        destroy: function () {
            $.removeData( document, 'i18n' );
        },

        /**
         * General message loading API This can take a URL string for
         * the json formatted messages. Example:
         * <code>load('path/to/all_localizations.json');</code>
         *
         * To load a localization file for a locale:
         * <code>
         * load('path/to/de-messages.json', 'de' );
         * </code>
         *
         * To load a localization file from a directory:
         * <code>
         * load('path/to/i18n/directory', 'de' );
         * </code>
         * The above method has the advantage of fallback resolution.
         * ie, it will automatically load the fallback locales for de.
         * For most usecases, this is the recommended method.
         * It is optional to have trailing slash at end.
         *
         * A data object containing message key- message translation mappings
         * can also be passed. Example:
         * <code>
         * load( { 'hello' : 'Hello' }, optionalLocale );
         * </code>
         *
         * A source map containing key-value pair of languagename and locations
         * can also be passed. Example:
         * <code>
         * load( {
         * bn: 'i18n/bn.json',
         * he: 'i18n/he.json',
         * en: 'i18n/en.json'
         * } )
         * </code>
         *
         * If the data argument is null/undefined/false,
         * all cached messages for the i18n instance will get reset.
         *
         * @param {string|Object} source
         * @param {string} locale Language tag
         * @return {jQuery.Promise}
         */
        load: function ( source, locale ) {
            var fallbackLocales, locIndex, fallbackLocale, sourceMap = {};
            if ( !source && !locale ) {
                source = 'i18n/' + $.i18n().locale + '.json';
                locale = $.i18n().locale;
            }
            if ( typeof source === 'string' &&
                // source extension should be json, but can have query params after that.
                source.split( '?' )[ 0 ].split( '.' ).pop() !== 'json'
            ) {
                // Load specified locale then check for fallbacks when directory is
                // specified in load()
                sourceMap[ locale ] = source + '/' + locale + '.json';
                fallbackLocales = ( $.i18n.fallbacks[ locale ] || [] )
                    .concat( this.options.fallbackLocale );
                for ( locIndex = 0; locIndex < fallbackLocales.length; locIndex++ ) {
                    fallbackLocale = fallbackLocales[ locIndex ];
                    sourceMap[ fallbackLocale ] = source + '/' + fallbackLocale + '.json';
                }
                return this.load( sourceMap );
            } else {
                return this.messageStore.load( source, locale );
            }

        },

        /**
         * Does parameter and magic word substitution.
         *
         * @param {string} key Message key
         * @param {Array} parameters Message parameters
         * @return {string}
         */
        parse: function ( key, parameters ) {
            var message = this.localize( key );
            // FIXME: This changes the state of the I18N object,
            // should probably not change the 'this.parser' but just
            // pass it to the parser.
            this.parser.language = $.i18n.languages[ $.i18n().locale ] || $.i18n.languages[ 'default' ];
            if ( message === '' ) {
                message = key;
            }
            return this.parser.parse( message, parameters );
        }
    };

    /**
     * Process a message from the $.I18N instance
     * for the current document, stored in jQuery.data(document).
     *
     * @param {string} key Key of the message.
     * @param {string} param1 [param...] Variadic list of parameters for {key}.
     * @return {string|$.I18N} Parsed message, or if no key was given
     * the instance of $.I18N is returned.
     */
    $.i18n = function ( key, param1 ) {
        var parameters,
            i18n = $.data( document, 'i18n' ),
            options = typeof key === 'object' && key;

        // If the locale option for this call is different then the setup so far,
        // update it automatically. This doesn't just change the context for this
        // call but for all future call as well.
        // If there is no i18n setup yet, don't do this. It will be taken care of
        // by the `new I18N` construction below.
        // NOTE: It should only change language for this one call.
        // Then cache instances of I18N somewhere.
        if ( options && options.locale && i18n && i18n.locale !== options.locale ) {
            i18n.locale = options.locale;
        }

        if ( !i18n ) {
            i18n = new I18N( options );
            $.data( document, 'i18n', i18n );
        }

        if ( typeof key === 'string' ) {
            if ( param1 !== undefined ) {
                parameters = slice.call( arguments, 1 );
            } else {
                parameters = [];
            }

            return i18n.parse( key, parameters );
        } else {
            // FIXME: remove this feature/bug.
            return i18n;
        }
    };

    $.fn.i18n = function () {
        var i18n = $.data( document, 'i18n' );

        if ( !i18n ) {
            i18n = new I18N();
            $.data( document, 'i18n', i18n );
        }

        return this.each( function () {
            var $this = $( this ),
                messageKey = $this.data( 'i18n' ),
                lBracket, rBracket, type, key;

            if ( messageKey ) {
                lBracket = messageKey.indexOf( '[' );
                rBracket = messageKey.indexOf( ']' );
                if ( lBracket !== -1 && rBracket !== -1 && lBracket < rBracket ) {
                    type = messageKey.slice( lBracket + 1, rBracket );
                    key = messageKey.slice( rBracket + 1 );
                    if ( type === 'html' ) {
                        $this.html( i18n.parse( key ) );
                    } else {
                        $this.attr( type, i18n.parse( key ) );
                    }
                } else {
                    $this.text( i18n.parse( messageKey ) );
                }
            } else {
                $this.find( '[data-i18n]' ).i18n();
            }
        } );
    };

    function getDefaultLocale() {
        var locale = $( 'html' ).attr( 'lang' );
        if ( !locale ) {
            locale = navigator.language || navigator.userLanguage || '';
        }
        return locale;
    }

    $.i18n.languages = {};
    $.i18n.messageStore = $.i18n.messageStore || {};
    $.i18n.parser = {
        // The default parser only handles variable substitution
        parse: function ( message, parameters ) {
            return message.replace( /\$(\d+)/g, function ( str, match ) {
                var index = parseInt( match, 10 ) - 1;
                return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
            } );
        },
        emitter: {}
    };
    $.i18n.fallbacks = {};
    $.i18n.debug = false;
    $.i18n.log = function ( /* arguments */ ) {
        if ( window.console && $.i18n.debug ) {
            window.console.log.apply( window.console, arguments );
        }
    };
    /* Static members */
    I18N.defaults = {
        locale: getDefaultLocale(),
        fallbackLocale: 'en',
        parser: $.i18n.parser,
        messageStore: $.i18n.messageStore
    };

    // Expose constructor
    $.i18n.constructor = I18N;
}( jQuery ) );