diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..def2089 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,3 @@ +{ + "i-message": "message" +} \ No newline at end of file diff --git a/i18n/zh.json b/i18n/zh.json new file mode 100644 index 0000000..1adb5fc --- /dev/null +++ b/i18n/zh.json @@ -0,0 +1,3 @@ +{ + "i-message": "信息" +} \ No newline at end of file diff --git a/index.html b/index.html index 3dea83c..85e9b7c 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,12 @@ + + + + + + @@ -80,6 +86,7 @@ Source Code Blog Install + @@ -504,10 +511,10 @@

Get latest news, keep updated.

diff --git a/scripts/jquery.i18n.emitter.js b/scripts/jquery.i18n.emitter.js new file mode 100644 index 0000000..330e50c --- /dev/null +++ b/scripts/jquery.i18n.emitter.js @@ -0,0 +1,168 @@ +/*! + * jQuery Internationalization library + * + * Copyright (C) 2011-2013 Santhosh Thottingal, Neil Kandalgaonkar + * + * 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 MessageParserEmitter = function () { + this.language = $.i18n.languages[ String.locale ] || $.i18n.languages[ 'default' ]; + }; + + MessageParserEmitter.prototype = { + constructor: MessageParserEmitter, + + /** + * (We put this method definition here, and not in prototype, to make + * sure it's not overwritten by any magic.) Walk entire node structure, + * applying replacements and template functions when appropriate + * + * @param {Mixed} node abstract syntax tree (top node or subnode) + * @param {Array} replacements for $1, $2, ... $n + * @return {Mixed} single-string node or array of nodes suitable for + * jQuery appending. + */ + emit: function ( node, replacements ) { + var ret, subnodes, operation, + messageParserEmitter = this; + + switch ( typeof node ) { + case 'string': + case 'number': + ret = node; + break; + case 'object': + // node is an array of nodes + subnodes = $.map( node.slice( 1 ), function ( n ) { + return messageParserEmitter.emit( n, replacements ); + } ); + + operation = node[ 0 ].toLowerCase(); + + if ( typeof messageParserEmitter[ operation ] === 'function' ) { + ret = messageParserEmitter[ operation ]( subnodes, replacements ); + } else { + throw new Error( 'unknown operation "' + operation + '"' ); + } + + break; + case 'undefined': + // Parsing the empty string (as an entire expression, or as a + // paramExpression in a template) results in undefined + // Perhaps a more clever parser can detect this, and return the + // empty string? Or is that useful information? + // The logical thing is probably to return the empty string here + // when we encounter undefined. + ret = ''; + break; + default: + throw new Error( 'unexpected type in AST: ' + typeof node ); + } + + return ret; + }, + + /** + * Parsing has been applied depth-first we can assume that all nodes + * here are single nodes Must return a single node to parents -- a + * jQuery with synthetic span However, unwrap any other synthetic spans + * in our children and pass them upwards + * + * @param {Array} nodes Mixed, some single nodes, some arrays of nodes. + * @return {string} + */ + concat: function ( nodes ) { + var result = ''; + + $.each( nodes, function ( i, node ) { + // strings, integers, anything else + result += node; + } ); + + return result; + }, + + /** + * Return escaped replacement of correct index, or string if + * unavailable. Note that we expect the parsed parameter to be + * zero-based. i.e. $1 should have become [ 0 ]. if the specified + * parameter is not found return the same string (e.g. "$99" -> + * parameter 98 -> not found -> return "$99" ) TODO throw error if + * nodes.length > 1 ? + * + * @param {Array} nodes One element, integer, n >= 0 + * @param {Array} replacements for $1, $2, ... $n + * @return {string} replacement + */ + replace: function ( nodes, replacements ) { + var index = parseInt( nodes[ 0 ], 10 ); + + if ( index < replacements.length ) { + // replacement is not a string, don't touch! + return replacements[ index ]; + } else { + // index not found, fallback to displaying variable + return '$' + ( index + 1 ); + } + }, + + /** + * Transform parsed structure into pluralization n.b. The first node may + * be a non-integer (for instance, a string representing an Arabic + * number). So convert it back with the current language's + * convertNumber. + * + * @param {Array} nodes List [ {String|Number}, {String}, {String} ... ] + * @return {string} selected pluralized form according to current + * language. + */ + plural: function ( nodes ) { + var count = parseFloat( this.language.convertNumber( nodes[ 0 ], 10 ) ), + forms = nodes.slice( 1 ); + + return forms.length ? this.language.convertPlural( count, forms ) : ''; + }, + + /** + * Transform parsed structure into gender Usage + * {{gender:gender|masculine|feminine|neutral}}. + * + * @param {Array} nodes List [ {String}, {String}, {String} , {String} ] + * @return {string} selected gender form according to current language + */ + gender: function ( nodes ) { + var gender = nodes[ 0 ], + forms = nodes.slice( 1 ); + + return this.language.gender( gender, forms ); + }, + + /** + * Transform parsed structure into grammar conversion. Invoked by + * putting {{grammar:form|word}} in a message + * + * @param {Array} nodes List [{Grammar case eg: genitive}, {String word}] + * @return {string} selected grammatical form according to current + * language. + */ + grammar: function ( nodes ) { + var form = nodes[ 0 ], + word = nodes[ 1 ]; + + return word && form && this.language.convertGrammar( word, form ); + } + }; + + $.extend( $.i18n.parser.emitter, new MessageParserEmitter() ); +}( jQuery ) ); diff --git a/scripts/jquery.i18n.fallbacks.js b/scripts/jquery.i18n.fallbacks.js new file mode 100644 index 0000000..74b16f2 --- /dev/null +++ b/scripts/jquery.i18n.fallbacks.js @@ -0,0 +1,186 @@ +/*! + * 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'; + + $.i18n = $.i18n || {}; + $.extend( $.i18n.fallbacks, { + ab: [ 'ru' ], + ace: [ 'id' ], + aln: [ 'sq' ], + // Not so standard - als is supposed to be Tosk Albanian, + // but in Wikipedia it's used for a Germanic language. + als: [ 'gsw', 'de' ], + an: [ 'es' ], + anp: [ 'hi' ], + arn: [ 'es' ], + arz: [ 'ar' ], + av: [ 'ru' ], + ay: [ 'es' ], + ba: [ 'ru' ], + bar: [ 'de' ], + 'bat-smg': [ 'sgs', 'lt' ], + bcc: [ 'fa' ], + 'be-x-old': [ 'be-tarask' ], + bh: [ 'bho' ], + bjn: [ 'id' ], + bm: [ 'fr' ], + bpy: [ 'bn' ], + bqi: [ 'fa' ], + bug: [ 'id' ], + 'cbk-zam': [ 'es' ], + ce: [ 'ru' ], + crh: [ 'crh-latn' ], + 'crh-cyrl': [ 'ru' ], + csb: [ 'pl' ], + cv: [ 'ru' ], + 'de-at': [ 'de' ], + 'de-ch': [ 'de' ], + 'de-formal': [ 'de' ], + dsb: [ 'de' ], + dtp: [ 'ms' ], + egl: [ 'it' ], + eml: [ 'it' ], + ff: [ 'fr' ], + fit: [ 'fi' ], + 'fiu-vro': [ 'vro', 'et' ], + frc: [ 'fr' ], + frp: [ 'fr' ], + frr: [ 'de' ], + fur: [ 'it' ], + gag: [ 'tr' ], + gan: [ 'gan-hant', 'zh-hant', 'zh-hans' ], + 'gan-hans': [ 'zh-hans' ], + 'gan-hant': [ 'zh-hant', 'zh-hans' ], + gl: [ 'pt' ], + glk: [ 'fa' ], + gn: [ 'es' ], + gsw: [ 'de' ], + hif: [ 'hif-latn' ], + hsb: [ 'de' ], + ht: [ 'fr' ], + ii: [ 'zh-cn', 'zh-hans' ], + inh: [ 'ru' ], + iu: [ 'ike-cans' ], + jut: [ 'da' ], + jv: [ 'id' ], + kaa: [ 'kk-latn', 'kk-cyrl' ], + kbd: [ 'kbd-cyrl' ], + khw: [ 'ur' ], + kiu: [ 'tr' ], + kk: [ 'kk-cyrl' ], + 'kk-arab': [ 'kk-cyrl' ], + 'kk-latn': [ 'kk-cyrl' ], + 'kk-cn': [ 'kk-arab', 'kk-cyrl' ], + 'kk-kz': [ 'kk-cyrl' ], + 'kk-tr': [ 'kk-latn', 'kk-cyrl' ], + kl: [ 'da' ], + 'ko-kp': [ 'ko' ], + koi: [ 'ru' ], + krc: [ 'ru' ], + ks: [ 'ks-arab' ], + ksh: [ 'de' ], + ku: [ 'ku-latn' ], + 'ku-arab': [ 'ckb' ], + kv: [ 'ru' ], + lad: [ 'es' ], + lb: [ 'de' ], + lbe: [ 'ru' ], + lez: [ 'ru' ], + li: [ 'nl' ], + lij: [ 'it' ], + liv: [ 'et' ], + lmo: [ 'it' ], + ln: [ 'fr' ], + ltg: [ 'lv' ], + lzz: [ 'tr' ], + mai: [ 'hi' ], + 'map-bms': [ 'jv', 'id' ], + mg: [ 'fr' ], + mhr: [ 'ru' ], + min: [ 'id' ], + mo: [ 'ro' ], + mrj: [ 'ru' ], + mwl: [ 'pt' ], + myv: [ 'ru' ], + mzn: [ 'fa' ], + nah: [ 'es' ], + nap: [ 'it' ], + nds: [ 'de' ], + 'nds-nl': [ 'nl' ], + 'nl-informal': [ 'nl' ], + no: [ 'nb' ], + os: [ 'ru' ], + pcd: [ 'fr' ], + pdc: [ 'de' ], + pdt: [ 'de' ], + pfl: [ 'de' ], + pms: [ 'it' ], + pt: [ 'pt-br' ], + 'pt-br': [ 'pt' ], + qu: [ 'es' ], + qug: [ 'qu', 'es' ], + rgn: [ 'it' ], + rmy: [ 'ro' ], + 'roa-rup': [ 'rup' ], + rue: [ 'uk', 'ru' ], + ruq: [ 'ruq-latn', 'ro' ], + 'ruq-cyrl': [ 'mk' ], + 'ruq-latn': [ 'ro' ], + sa: [ 'hi' ], + sah: [ 'ru' ], + scn: [ 'it' ], + sg: [ 'fr' ], + sgs: [ 'lt' ], + sli: [ 'de' ], + sr: [ 'sr-ec' ], + srn: [ 'nl' ], + stq: [ 'de' ], + su: [ 'id' ], + szl: [ 'pl' ], + tcy: [ 'kn' ], + tg: [ 'tg-cyrl' ], + tt: [ 'tt-cyrl', 'ru' ], + 'tt-cyrl': [ 'ru' ], + ty: [ 'fr' ], + udm: [ 'ru' ], + ug: [ 'ug-arab' ], + uk: [ 'ru' ], + vec: [ 'it' ], + vep: [ 'et' ], + vls: [ 'nl' ], + vmf: [ 'de' ], + vot: [ 'fi' ], + vro: [ 'et' ], + wa: [ 'fr' ], + wo: [ 'fr' ], + wuu: [ 'zh-hans' ], + xal: [ 'ru' ], + xmf: [ 'ka' ], + yi: [ 'he' ], + za: [ 'zh-hans' ], + zea: [ 'nl' ], + zh: [ 'zh-hans' ], + 'zh-classical': [ 'lzh' ], + 'zh-cn': [ 'zh-hans' ], + 'zh-hant': [ 'zh-hans' ], + 'zh-hk': [ 'zh-hant', 'zh-hans' ], + 'zh-min-nan': [ 'nan' ], + 'zh-mo': [ 'zh-hk', 'zh-hant', 'zh-hans' ], + 'zh-my': [ 'zh-sg', 'zh-hans' ], + 'zh-sg': [ 'zh-hans' ], + 'zh-tw': [ 'zh-hant', 'zh-hans' ], + 'zh-yue': [ 'yue' ] + } ); +}( jQuery ) ); diff --git a/scripts/jquery.i18n.js b/scripts/jquery.i18n.js new file mode 100644 index 0000000..d6c0bae --- /dev/null +++ b/scripts/jquery.i18n.js @@ -0,0 +1,290 @@ +/*! + * 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 ) ); diff --git a/scripts/jquery.i18n.language.js b/scripts/jquery.i18n.language.js new file mode 100644 index 0000000..daabfaf --- /dev/null +++ b/scripts/jquery.i18n.language.js @@ -0,0 +1,499 @@ +/* global pluralRuleParser */ +( function ( $ ) { + 'use strict'; + + // jscs:disable + var language = { + // CLDR plural rules generated using + // libs/CLDRPluralRuleParser/tools/PluralXML2JSON.html + pluralRules: { + ak: { + one: 'n = 0..1' + }, + am: { + one: 'i = 0 or n = 1' + }, + ar: { + zero: 'n = 0', + one: 'n = 1', + two: 'n = 2', + few: 'n % 100 = 3..10', + many: 'n % 100 = 11..99' + }, + ars: { + zero: 'n = 0', + one: 'n = 1', + two: 'n = 2', + few: 'n % 100 = 3..10', + many: 'n % 100 = 11..99' + }, + as: { + one: 'i = 0 or n = 1' + }, + be: { + one: 'n % 10 = 1 and n % 100 != 11', + few: 'n % 10 = 2..4 and n % 100 != 12..14', + many: 'n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14' + }, + bh: { + one: 'n = 0..1' + }, + bn: { + one: 'i = 0 or n = 1' + }, + br: { + one: 'n % 10 = 1 and n % 100 != 11,71,91', + two: 'n % 10 = 2 and n % 100 != 12,72,92', + few: 'n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99', + many: 'n != 0 and n % 1000000 = 0' + }, + bs: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14' + }, + cs: { + one: 'i = 1 and v = 0', + few: 'i = 2..4 and v = 0', + many: 'v != 0' + }, + cy: { + zero: 'n = 0', + one: 'n = 1', + two: 'n = 2', + few: 'n = 3', + many: 'n = 6' + }, + da: { + one: 'n = 1 or t != 0 and i = 0,1' + }, + dsb: { + one: 'v = 0 and i % 100 = 1 or f % 100 = 1', + two: 'v = 0 and i % 100 = 2 or f % 100 = 2', + few: 'v = 0 and i % 100 = 3..4 or f % 100 = 3..4' + }, + fa: { + one: 'i = 0 or n = 1' + }, + ff: { + one: 'i = 0,1' + }, + fil: { + one: 'v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9' + }, + fr: { + one: 'i = 0,1' + }, + ga: { + one: 'n = 1', + two: 'n = 2', + few: 'n = 3..6', + many: 'n = 7..10' + }, + gd: { + one: 'n = 1,11', + two: 'n = 2,12', + few: 'n = 3..10,13..19' + }, + gu: { + one: 'i = 0 or n = 1' + }, + guw: { + one: 'n = 0..1' + }, + gv: { + one: 'v = 0 and i % 10 = 1', + two: 'v = 0 and i % 10 = 2', + few: 'v = 0 and i % 100 = 0,20,40,60,80', + many: 'v != 0' + }, + he: { + one: 'i = 1 and v = 0', + two: 'i = 2 and v = 0', + many: 'v = 0 and n != 0..10 and n % 10 = 0' + }, + hi: { + one: 'i = 0 or n = 1' + }, + hr: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14' + }, + hsb: { + one: 'v = 0 and i % 100 = 1 or f % 100 = 1', + two: 'v = 0 and i % 100 = 2 or f % 100 = 2', + few: 'v = 0 and i % 100 = 3..4 or f % 100 = 3..4' + }, + hy: { + one: 'i = 0,1' + }, + is: { + one: 't = 0 and i % 10 = 1 and i % 100 != 11 or t != 0' + }, + iu: { + one: 'n = 1', + two: 'n = 2' + }, + iw: { + one: 'i = 1 and v = 0', + two: 'i = 2 and v = 0', + many: 'v = 0 and n != 0..10 and n % 10 = 0' + }, + kab: { + one: 'i = 0,1' + }, + kn: { + one: 'i = 0 or n = 1' + }, + kw: { + one: 'n = 1', + two: 'n = 2' + }, + lag: { + zero: 'n = 0', + one: 'i = 0,1 and n != 0' + }, + ln: { + one: 'n = 0..1' + }, + lt: { + one: 'n % 10 = 1 and n % 100 != 11..19', + few: 'n % 10 = 2..9 and n % 100 != 11..19', + many: 'f != 0' + }, + lv: { + zero: 'n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19', + one: 'n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1' + }, + mg: { + one: 'n = 0..1' + }, + mk: { + one: 'v = 0 and i % 10 = 1 or f % 10 = 1' + }, + mo: { + one: 'i = 1 and v = 0', + few: 'v != 0 or n = 0 or n != 1 and n % 100 = 1..19' + }, + mr: { + one: 'i = 0 or n = 1' + }, + mt: { + one: 'n = 1', + few: 'n = 0 or n % 100 = 2..10', + many: 'n % 100 = 11..19' + }, + naq: { + one: 'n = 1', + two: 'n = 2' + }, + nso: { + one: 'n = 0..1' + }, + pa: { + one: 'n = 0..1' + }, + pl: { + one: 'i = 1 and v = 0', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14', + many: 'v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14' + }, + prg: { + zero: 'n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19', + one: 'n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1' + }, + pt: { + one: 'i = 0..1' + }, + ro: { + one: 'i = 1 and v = 0', + few: 'v != 0 or n = 0 or n != 1 and n % 100 = 1..19' + }, + ru: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14', + many: 'v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14' + }, + se: { + one: 'n = 1', + two: 'n = 2' + }, + sh: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14' + }, + shi: { + one: 'i = 0 or n = 1', + few: 'n = 2..10' + }, + si: { + one: 'n = 0,1 or i = 0 and f = 1' + }, + sk: { + one: 'i = 1 and v = 0', + few: 'i = 2..4 and v = 0', + many: 'v != 0' + }, + sl: { + one: 'v = 0 and i % 100 = 1', + two: 'v = 0 and i % 100 = 2', + few: 'v = 0 and i % 100 = 3..4 or v != 0' + }, + sma: { + one: 'n = 1', + two: 'n = 2' + }, + smi: { + one: 'n = 1', + two: 'n = 2' + }, + smj: { + one: 'n = 1', + two: 'n = 2' + }, + smn: { + one: 'n = 1', + two: 'n = 2' + }, + sms: { + one: 'n = 1', + two: 'n = 2' + }, + sr: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14' + }, + ti: { + one: 'n = 0..1' + }, + tl: { + one: 'v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9' + }, + tzm: { + one: 'n = 0..1 or n = 11..99' + }, + uk: { + one: 'v = 0 and i % 10 = 1 and i % 100 != 11', + few: 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14', + many: 'v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14' + }, + wa: { + one: 'n = 0..1' + }, + zu: { + one: 'i = 0 or n = 1' + } + }, + // jscs:enable + + /** + * Plural form transformations, needed for some languages. + * + * @param {integer} count + * Non-localized quantifier + * @param {Array} forms + * List of plural forms + * @return {string} Correct form for quantifier in this language + */ + convertPlural: function ( count, forms ) { + var pluralRules, + pluralFormIndex, + index, + explicitPluralPattern = new RegExp( '\\d+=', 'i' ), + formCount, + form; + + if ( !forms || forms.length === 0 ) { + return ''; + } + + // Handle for Explicit 0= & 1= values + for ( index = 0; index < forms.length; index++ ) { + form = forms[ index ]; + if ( explicitPluralPattern.test( form ) ) { + formCount = parseInt( form.slice( 0, form.indexOf( '=' ) ), 10 ); + if ( formCount === count ) { + return ( form.slice( form.indexOf( '=' ) + 1 ) ); + } + forms[ index ] = undefined; + } + } + + forms = $.map( forms, function ( form ) { + if ( form !== undefined ) { + return form; + } + } ); + + pluralRules = this.pluralRules[ $.i18n().locale ]; + + if ( !pluralRules ) { + // default fallback. + return ( count === 1 ) ? forms[ 0 ] : forms[ 1 ]; + } + + pluralFormIndex = this.getPluralForm( count, pluralRules ); + pluralFormIndex = Math.min( pluralFormIndex, forms.length - 1 ); + + return forms[ pluralFormIndex ]; + }, + + /** + * For the number, get the plural for index + * + * @param {integer} number + * @param {Object} pluralRules + * @return {integer} plural form index + */ + getPluralForm: function ( number, pluralRules ) { + var i, + pluralForms = [ 'zero', 'one', 'two', 'few', 'many', 'other' ], + pluralFormIndex = 0; + + for ( i = 0; i < pluralForms.length; i++ ) { + if ( pluralRules[ pluralForms[ i ] ] ) { + if ( pluralRuleParser( pluralRules[ pluralForms[ i ] ], number ) ) { + return pluralFormIndex; + } + + pluralFormIndex++; + } + } + + return pluralFormIndex; + }, + + /** + * Converts a number using digitTransformTable. + * + * @param {number} num Value to be converted + * @param {boolean} integer Convert the return value to an integer + * @return {string} The number converted into a String. + */ + convertNumber: function ( num, integer ) { + var tmp, item, i, + transformTable, numberString, convertedNumber; + + // Set the target Transform table: + transformTable = this.digitTransformTable( $.i18n().locale ); + numberString = String( num ); + convertedNumber = ''; + + if ( !transformTable ) { + return num; + } + + // Check if the restore to Latin number flag is set: + if ( integer ) { + if ( parseFloat( num, 10 ) === num ) { + return num; + } + + tmp = []; + + for ( item in transformTable ) { + tmp[ transformTable[ item ] ] = item; + } + + transformTable = tmp; + } + + for ( i = 0; i < numberString.length; i++ ) { + if ( transformTable[ numberString[ i ] ] ) { + convertedNumber += transformTable[ numberString[ i ] ]; + } else { + convertedNumber += numberString[ i ]; + } + } + + return integer ? parseFloat( convertedNumber, 10 ) : convertedNumber; + }, + + /** + * Grammatical transformations, needed for inflected languages. + * Invoked by putting {{grammar:form|word}} in a message. + * Override this method for languages that need special grammar rules + * applied dynamically. + * + * @param {string} word + * @param {string} form + * @return {string} + */ + // eslint-disable-next-line no-unused-vars + convertGrammar: function ( word, form ) { + return word; + }, + + /** + * Provides an alternative text depending on specified gender. Usage + * {{gender:[gender|user object]|masculine|feminine|neutral}}. If second + * or third parameter are not specified, masculine is used. + * + * These details may be overriden per language. + * + * @param {string} gender + * male, female, or anything else for neutral. + * @param {Array} forms + * List of gender forms + * + * @return {string} + */ + gender: function ( gender, forms ) { + if ( !forms || forms.length === 0 ) { + return ''; + } + + while ( forms.length < 2 ) { + forms.push( forms[ forms.length - 1 ] ); + } + + if ( gender === 'male' ) { + return forms[ 0 ]; + } + + if ( gender === 'female' ) { + return forms[ 1 ]; + } + + return ( forms.length === 3 ) ? forms[ 2 ] : forms[ 0 ]; + }, + + /** + * Get the digit transform table for the given language + * See http://cldr.unicode.org/translation/numbering-systems + * + * @param {string} language + * @return {Array|boolean} List of digits in the passed language or false + * representation, or boolean false if there is no information. + */ + digitTransformTable: function ( language ) { + var tables = { + ar: '٠١٢٣٤٥٦٧٨٩', + fa: '۰۱۲۳۴۵۶۷۸۹', + ml: '൦൧൨൩൪൫൬൭൮൯', + kn: '೦೧೨೩೪೫೬೭೮೯', + lo: '໐໑໒໓໔໕໖໗໘໙', + or: '୦୧୨୩୪୫୬୭୮୯', + kh: '០១២៣៤៥៦៧៨៩', + nqo: '߀߁߂߃߄߅߆߇߈߉', // Note that the digits go right to left + pa: '੦੧੨੩੪੫੬੭੮੯', + gu: '૦૧૨૩૪૫૬૭૮૯', + hi: '०१२३४५६७८९', + my: '၀၁၂၃၄၅၆၇၈၉', + ta: '௦௧௨௩௪௫௬௭௮௯', + te: '౦౧౨౩౪౫౬౭౮౯', + th: '๐๑๒๓๔๕๖๗๘๙', // FIXME use iso 639 codes + bo: '༠༡༢༣༤༥༦༧༨༩' // FIXME use iso 639 codes + }; + + if ( !tables[ language ] ) { + return false; + } + + return tables[ language ].split( '' ); + } + }; + + $.extend( $.i18n.languages, { + default: language + } ); +}( jQuery ) ); diff --git a/scripts/jquery.i18n.messagestore.js b/scripts/jquery.i18n.messagestore.js new file mode 100644 index 0000000..ba81ce5 --- /dev/null +++ b/scripts/jquery.i18n.messagestore.js @@ -0,0 +1,123 @@ +/*! + * jQuery Internationalization library - Message Store + * + * 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 MessageStore = function () { + this.messages = {}; + this.sources = {}; + }; + + function jsonMessageLoader( url ) { + var deferred = $.Deferred(); + + $.getJSON( url ) + .done( deferred.resolve ) + .fail( function ( jqxhr, settings, exception ) { + $.i18n.log( 'Error in loading messages from ' + url + ' Exception: ' + exception ); + // Ignore 404 exception, because we are handling fallabacks explicitly + deferred.resolve(); + } ); + + return deferred.promise(); + } + + /** + * See https://github.com/wikimedia/jquery.i18n/wiki/Specification#wiki-Message_File_Loading + */ + MessageStore.prototype = { + + /** + * General message loading API This can take a URL string for + * the json formatted messages. + * load('path/to/all_localizations.json'); + * + * This can also load a localization file for a locale + * load( 'path/to/de-messages.json', 'de' ); + * + * A data object containing message key- message translation mappings + * can also be passed Eg: + * + * load( { 'hello' : 'Hello' }, optionalLocale ); + * 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 key = null, + deferreds = [], + messageStore = this; + + if ( typeof source === 'string' ) { + // This is a URL to the messages file. + $.i18n.log( 'Loading messages from: ' + source ); + return jsonMessageLoader( source ) + .then( function ( localization ) { + return messageStore.load( localization, locale ); + } ); + } + + if ( locale ) { + // source is an key-value pair of messages for given locale + messageStore.set( locale, source ); + + return $.Deferred().resolve(); + } else { + // source is a key-value pair of locales and their source + for ( key in source ) { + if ( Object.prototype.hasOwnProperty.call( source, key ) ) { + locale = key; + // No {locale} given, assume data is a group of languages, + // call this function again for each language. + deferreds.push( messageStore.load( source[ key ], locale ) ); + } + } + return $.when.apply( $, deferreds ); + } + + }, + + /** + * Set messages to the given locale. + * If locale exists, add messages to the locale. + * + * @param {string} locale + * @param {Object} messages + */ + set: function ( locale, messages ) { + if ( !this.messages[ locale ] ) { + this.messages[ locale ] = messages; + } else { + this.messages[ locale ] = $.extend( this.messages[ locale ], messages ); + } + }, + + /** + * + * @param {string} locale + * @param {string} messageKey + * @return {boolean} + */ + get: function ( locale, messageKey ) { + return this.messages[ locale ] && this.messages[ locale ][ messageKey ]; + } + }; + + $.extend( $.i18n.messageStore, new MessageStore() ); +}( jQuery ) ); diff --git a/scripts/jquery.i18n.parser.js b/scripts/jquery.i18n.parser.js new file mode 100644 index 0000000..8c47e55 --- /dev/null +++ b/scripts/jquery.i18n.parser.js @@ -0,0 +1,310 @@ +/*! + * jQuery Internationalization library + * + * Copyright (C) 2011-2013 Santhosh Thottingal, Neil Kandalgaonkar + * + * 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 MessageParser = function ( options ) { + this.options = $.extend( {}, $.i18n.parser.defaults, options ); + this.language = $.i18n.languages[ String.locale ] || $.i18n.languages[ 'default' ]; + this.emitter = $.i18n.parser.emitter; + }; + + MessageParser.prototype = { + + constructor: MessageParser, + + simpleParse: function ( message, parameters ) { + return message.replace( /\$(\d+)/g, function ( str, match ) { + var index = parseInt( match, 10 ) - 1; + + return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; + } ); + }, + + parse: function ( message, replacements ) { + if ( message.indexOf( '{{' ) < 0 ) { + return this.simpleParse( message, replacements ); + } + + this.emitter.language = $.i18n.languages[ $.i18n().locale ] || + $.i18n.languages[ 'default' ]; + + return this.emitter.emit( this.ast( message ), replacements ); + }, + + ast: function ( message ) { + var pipe, colon, backslash, anyCharacter, dollar, digits, regularLiteral, + regularLiteralWithoutBar, regularLiteralWithoutSpace, escapedOrLiteralWithoutBar, + escapedOrRegularLiteral, templateContents, templateName, openTemplate, + closeTemplate, expression, paramExpression, result, + pos = 0; + + // Try parsers until one works, if none work return null + function choice( parserSyntax ) { + return function () { + var i, result; + + for ( i = 0; i < parserSyntax.length; i++ ) { + result = parserSyntax[ i ](); + + if ( result !== null ) { + return result; + } + } + + return null; + }; + } + + // Try several parserSyntax-es in a row. + // All must succeed; otherwise, return null. + // This is the only eager one. + function sequence( parserSyntax ) { + var i, res, + originalPos = pos, + result = []; + + for ( i = 0; i < parserSyntax.length; i++ ) { + res = parserSyntax[ i ](); + + if ( res === null ) { + pos = originalPos; + + return null; + } + + result.push( res ); + } + + return result; + } + + // Run the same parser over and over until it fails. + // Must succeed a minimum of n times; otherwise, return null. + function nOrMore( n, p ) { + return function () { + var originalPos = pos, + result = [], + parsed = p(); + + while ( parsed !== null ) { + result.push( parsed ); + parsed = p(); + } + + if ( result.length < n ) { + pos = originalPos; + + return null; + } + + return result; + }; + } + + // Helpers -- just make parserSyntax out of simpler JS builtin types + + function makeStringParser( s ) { + var len = s.length; + + return function () { + var result = null; + + if ( message.slice( pos, pos + len ) === s ) { + result = s; + pos += len; + } + + return result; + }; + } + + function makeRegexParser( regex ) { + return function () { + var matches = message.slice( pos ).match( regex ); + + if ( matches === null ) { + return null; + } + + pos += matches[ 0 ].length; + + return matches[ 0 ]; + }; + } + + pipe = makeStringParser( '|' ); + colon = makeStringParser( ':' ); + backslash = makeStringParser( '\\' ); + anyCharacter = makeRegexParser( /^./ ); + dollar = makeStringParser( '$' ); + digits = makeRegexParser( /^\d+/ ); + regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ ); + regularLiteralWithoutBar = makeRegexParser( /^[^{}[\]$\\|]/ ); + regularLiteralWithoutSpace = makeRegexParser( /^[^{}[\]$\s]/ ); + + // There is a general pattern: + // parse a thing; + // if it worked, apply transform, + // otherwise return null. + // But using this as a combinator seems to cause problems + // when combined with nOrMore(). + // May be some scoping issue. + function transform( p, fn ) { + return function () { + var result = p(); + + return result === null ? null : fn( result ); + }; + } + + // Used to define "literals" within template parameters. The pipe + // character is the parameter delimeter, so by default + // it is not a literal in the parameter + function literalWithoutBar() { + var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); + + return result === null ? null : result.join( '' ); + } + + function literal() { + var result = nOrMore( 1, escapedOrRegularLiteral )(); + + return result === null ? null : result.join( '' ); + } + + function escapedLiteral() { + var result = sequence( [ backslash, anyCharacter ] ); + + return result === null ? null : result[ 1 ]; + } + + choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); + escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); + escapedOrRegularLiteral = choice( [ escapedLiteral, regularLiteral ] ); + + function replacement() { + var result = sequence( [ dollar, digits ] ); + + if ( result === null ) { + return null; + } + + return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ]; + } + + templateName = transform( + // see $wgLegalTitleChars + // not allowing : due to the need to catch "PLURAL:$1" + makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ), + + function ( result ) { + return result.toString(); + } + ); + + function templateParam() { + var expr, + result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] ); + + if ( result === null ) { + return null; + } + + expr = result[ 1 ]; + + // use a "CONCAT" operator if there are multiple nodes, + // otherwise return the first node, raw. + return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ]; + } + + function templateWithReplacement() { + var result = sequence( [ templateName, colon, replacement ] ); + + return result === null ? null : [ result[ 0 ], result[ 2 ] ]; + } + + function templateWithOutReplacement() { + var result = sequence( [ templateName, colon, paramExpression ] ); + + return result === null ? null : [ result[ 0 ], result[ 2 ] ]; + } + + templateContents = choice( [ + function () { + var res = sequence( [ + // templates can have placeholders for dynamic + // replacement eg: {{PLURAL:$1|one car|$1 cars}} + // or no placeholders eg: + // {{GRAMMAR:genitive|{{SITENAME}}} + choice( [ templateWithReplacement, templateWithOutReplacement ] ), + nOrMore( 0, templateParam ) + ] ); + + return res === null ? null : res[ 0 ].concat( res[ 1 ] ); + }, + function () { + var res = sequence( [ templateName, nOrMore( 0, templateParam ) ] ); + + if ( res === null ) { + return null; + } + + return [ res[ 0 ] ].concat( res[ 1 ] ); + } + ] ); + + openTemplate = makeStringParser( '{{' ); + closeTemplate = makeStringParser( '}}' ); + + function template() { + var result = sequence( [ openTemplate, templateContents, closeTemplate ] ); + + return result === null ? null : result[ 1 ]; + } + + expression = choice( [ template, replacement, literal ] ); + paramExpression = choice( [ template, replacement, literalWithoutBar ] ); + + function start() { + var result = nOrMore( 0, expression )(); + + if ( result === null ) { + return null; + } + + return [ 'CONCAT' ].concat( result ); + } + + result = start(); + + /* + * For success, the pos must have gotten to the end of the input + * and returned a non-null. + * n.b. This is part of language infrastructure, so we do not throw an + * internationalizable message. + */ + if ( result === null || pos !== message.length ) { + throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + message ); + } + + return result; + } + + }; + + $.extend( $.i18n.parser, new MessageParser() ); +}( jQuery ) ); diff --git a/scripts/main.js b/scripts/main.js index d676dd6..0074c88 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,4 +1,15 @@ jQuery(document).ready(function($) { + var i18n = $.i18n() + i18n.load({ + en: 'i18n/en.json', + zh: 'i18n/zh.json' + }) + var localStorageLang = window.localStorage.getItem('mask_language') + var lang = localStorageLang ? + localStorageLang : ['zh', 'en'].includes(navigator.language) ? + navigator.language : 'en' + setLocale(lang) + $('a[href$="/install"]').attr("href", function() { const agent = navigator.userAgent; if (/Chrome/i.test(agent)) { @@ -67,4 +78,20 @@ jQuery(document).ready(function($) { setCardGroup($(this).index()); }); }); + + $('#i18n-en').on("click", function() { + setLocale('en', true) + }) + + $('#i18n-zh').on("click", function() { + setLocale('zh', true) + }) + + function setLocale (lang, cache = false) { + setTimeout(() => { + i18n.locale = lang + if (cache) window.localStorage.setItem('mask_language', lang) + $('body').i18n() + }, 1000) + } });