/*! * 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: * load('path/to/all_localizations.json'); * * To load a localization file for a locale: * * load('path/to/de-messages.json', 'de' ); * * * To load a localization file from a directory: * * load('path/to/i18n/directory', 'de' ); * * 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: * * load( { 'hello' : 'Hello' }, optionalLocale ); * * * A source map containing key-value pair of languagename and locations * can also be passed. Example: * * load( { * bn: 'i18n/bn.json', * he: 'i18n/he.json', * en: 'i18n/en.json' * } ) * * * 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 ) );