├── .travis.yml ├── README.md ├── dist ├── mithril-translate.js ├── mithril-translate.js.map └── mithril-translate.min.js ├── gruntfile.js ├── package.json ├── src └── mithril-translate.js └── test ├── i18n └── translations.js ├── lib └── mithril │ ├── mithril.js │ ├── mithril.min.js │ ├── mithril.min.js.map │ └── mock.js └── mithril-translate.test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10.32" 4 | before_install: npm install -g grunt-cli 5 | install: npm install 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mithril Translate [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/kawan16/mithril-translate.svg?branch=master)](https://travis-ci.org/kawan16/mithril-translate) [![Code Climate](https://codeclimate.com/github/kawan16/mithril-translate/badges/gpa.svg)](https://codeclimate.com/github/kawan16/mithril-translate) 5 | 6 | Mithril Translate is a library which allows to internationalize your mithril applications including lazy loading and variable replacement. 7 | 8 | ## Get Started 9 | 10 | One way to use Mithril Translate: download this project, get the `dist` folder files and link to mithril and mithril-translate in the head of your app: 11 | 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | ## Demo 21 | 22 | Take a look at http://kawan16.github.io/mithril-translate/ and see Mithril-translate in action. 23 | 24 | ## How to use it 25 | 26 | ### The `mx.translate.configure` function 27 | 28 | First, you need to initialize the translations environment mainly by calling the `mx.translate.configure` and passing it configuration options. 29 | 30 | ```js 31 | mx.translate.configure( { infix: '/i18n/' , suffix: '.json' } ); 32 | ``` 33 | The method accepts the following options: 34 | 35 | * *infix* : the infix string which allows to access to remote translations files 36 | * *suffix* : the suffix of the translation files 37 | 38 | ### The `mx.translate.use` function 39 | 40 | The `mx.translate.use` function sets ( or returns ) the current translation language. By calling it with a given language path filename, the related translation file will be loaded ( if it has not been done yet ) by using the `infix` and `suffix` from the configuration options.Note that the function will return the promise resolved when the file has been loaded. 41 | 42 | ```js 43 | mx.translate.configure( { infix: '/i18n/' , suffix: '.json' } ); 44 | mx.translate.use( 'en' ) // Load the translation file located at '/i18n/en.json' 45 | .then( function( ) { /* Do some translation for instance, or mount your Mithril component */ } ); 46 | mx.translate.use(); // Return 'en' 47 | ``` 48 | 49 | The `mx.translate.use` expects to get an javascript object which contains translations through its properties such as: 50 | ```js 51 | { 52 | 'home' : 'Home', 53 | 'login' : 'Login', 54 | 'component' : { 55 | 'widgetA-title' : 'Widget A', 56 | 'widgetB-title' : 'Widget B' 57 | } 58 | } 59 | ``` 60 | 61 | #### Inline translations 62 | 63 | You may use local/inline/static translations for a given language by passing a translation object as second paramater of the `mx.translate.use` function: 64 | 65 | ```js 66 | // In this case, no need to configure infix or suffix 67 | 68 | var enTranslations = { 69 | hello: 'Hello !' 70 | }; 71 | mx.translate.use( 'en' , enTranslations ); 72 | mx.translate( 'hello' ); // returns 'Hello !' 73 | 74 | ``` 75 | 76 | ### The `mx.translate` function 77 | 78 | Once configuring and setting a language, the `mx.translate` is very easy to use. Given the property path, the function returns the translation. 79 | 80 | ```js 81 | // Suppose the above translation file 82 | mx.translate( 'home' ); // Returns 'Home' 83 | mx.translate( 'component.widgetA-title' ); // Returns 'Widget A' 84 | ``` 85 | 86 | #### Variable replacement 87 | 88 | Translation content can contain variables which will be instantiated at translation call. We use the interpolation symbols `{{}}` in order specify a variable. We pass the variable settings in a key / value object as the second paramater of the `mx.translate` function. Suppose: 89 | 90 | ```js 91 | // In the translation file 92 | { 93 | 'welcome' : 'Welcome {{name}}', 94 | } 95 | 96 | // In the application js file 97 | mx.translate( 'welcome' , { name: 'Kawan16' } ); // Returns 'Welcome Kawan16' 98 | 99 | ``` 100 | 101 | #### Pluralization 102 | 103 | You can define and use pluralization ( and mix it with variable replacement ). 104 | 105 | ```js 106 | // In the translation file 107 | { 108 | 'viewing' : { 109 | '0' : 'Nobody is viewing', 110 | '1' : '{{ person1 }} is viewing', 111 | '2' : '{{ person1 }} and {{ person2 }} are viewing', 112 | 'other' : 'Several persons are viewing' 113 | } 114 | } 115 | 116 | // In the application js file 117 | 118 | // Only pluralization index 119 | mx.translate( 'viewing' , '0' ); // Returns 'Nobody is viewing' 120 | 121 | // Pluralization index and variable assignment 122 | mx.translate( 'viewing' , { person1: 'Kawan16' } , '1' ); // Returns 'Kawan16 is viewing' 123 | mx.translate( 'viewing' , { person1: 'Kawan16' , person2: 'Toto' } , '2' ); // Returns 'Kawan16 and Toto are viewing' 124 | 125 | ``` 126 | 127 | ## History 128 | 129 | * 0.1.0 - Initial Release 130 | * 0.1.1 - Pluralization 131 | * 0.1.2 - Fix [#2](https://github.com/kawan16/mithril-translate/issues/2) / Synchronized translations loading 132 | 133 | ## License 134 | 135 | Licensed under the MIT license. 136 | -------------------------------------------------------------------------------- /dist/mithril-translate.js: -------------------------------------------------------------------------------- 1 | /* global m, mx */ 2 | 3 | var mx_factory = function( m ) { 4 | 'use strict'; 5 | 6 | var mx = {}; 7 | 8 | /** 9 | * Storage constructor 10 | */ 11 | var Storage = function( ) { 12 | var store = {}; 13 | var variableRegex = /\{\{(.*?)\}\}/g; 14 | return { 15 | 16 | /** 17 | * Stores the given translation object in the current language cache 18 | * @param {object} The translation object 19 | */ 20 | set: function( item ) { 21 | store[ currentLanguage ] = item; 22 | }, 23 | 24 | /** 25 | * Returns the translation given the current language, the given name property and the optional 26 | * values replacement in case of template message 27 | * @param {String} The name of translation property 28 | * @param {object} The key/value map of variable / values to replace in the translation content 29 | */ 30 | get: function( name , values ) { 31 | var translations = store[ currentLanguage ]; 32 | var result = translations; 33 | name 34 | .split('.') 35 | .forEach( function( property ) { 36 | result = result[ property ]; 37 | }); 38 | if( values ) { 39 | var variables = result.match( variableRegex ); 40 | for( var key in values ) { 41 | variables.forEach( function( variable ) { 42 | if( variable.indexOf( key ) !== -1 ) { 43 | result = result.replace( variable , values[ key ] ); 44 | } 45 | }) 46 | } 47 | } 48 | return result; 49 | } 50 | } 51 | }; 52 | 53 | 54 | /* 55 | * Validators for router function 56 | */ 57 | var validators = { 58 | /** 59 | * Check whether the given parameter is a string 60 | * @param {String} string 61 | * @returns {String} value 62 | * @throws {TypeError} for non strings 63 | */ 64 | string : function(string){ 65 | if(typeof string !== 'string'){ 66 | throw new TypeError('a string is expected, but ' + string + ' [' + (typeof string) + '] given'); 67 | } 68 | return string; 69 | }, 70 | 71 | /** 72 | * Check whether the given parameter is a plain object (array and functions aren't accepted) 73 | * @param {Object} object 74 | * @returns {Object} object 75 | * @throws {TypeError} for non object 76 | */ 77 | plainObject : function(object){ 78 | if(typeof object !== 'object' ){ 79 | throw new TypeError('an object is expected, but ' + object + ' [' + (typeof object) + '] given'); 80 | } 81 | return object; 82 | } 83 | }; 84 | 85 | /** 86 | * The module configuration 87 | */ 88 | var configuration; 89 | 90 | /** 91 | * The current language 92 | */ 93 | var currentLanguage; 94 | 95 | /** 96 | * The translation storage 97 | */ 98 | var storage = new Storage(); 99 | 100 | /** 101 | * Returns the translation of a given item name with optional variable/value substitution 102 | */ 103 | mx.translate = function( item ) { 104 | validators.string( item ); 105 | if( arguments.length > 1 ) { 106 | var secondParameter = arguments[ 1 ]; 107 | if( arguments.length === 3 ) { 108 | var thirdParameter = arguments[ 2 ]; 109 | return storage.get( item + '.' + thirdParameter , secondParameter ); 110 | } else if( typeof secondParameter === 'object' ) { 111 | return storage.get( item , secondParameter ); 112 | } else { 113 | return storage.get( item + '.' + secondParameter ); 114 | } 115 | } 116 | else { 117 | return storage.get( item ); 118 | } 119 | }; 120 | 121 | /** 122 | * Loads the translations files for the given folder 123 | */ 124 | mx.translate.configure = function( options ) { 125 | validators.plainObject( options ); 126 | configuration = options; 127 | }; 128 | 129 | /** 130 | * Get / Set the language 131 | */ 132 | mx.translate.use = function( languageToUse , optionalTranslations ) { 133 | if( languageToUse && currentLanguage !== languageToUse ) { 134 | validators.string( languageToUse ); 135 | if( optionalTranslations ) { 136 | $store( languageToUse , optionalTranslations ); 137 | } else { 138 | return $load( languageToUse ); 139 | } 140 | } else { 141 | return currentLanguage; 142 | } 143 | }; 144 | 145 | /** 146 | * Stores static translations for a given language 147 | */ 148 | function $store( languageToUse , translationsToStore ) { 149 | if( isBrowser( ) ) { m.startComputation(); } 150 | currentLanguage = languageToUse || currentLanguage; 151 | storage.set( translationsToStore ); 152 | if( isBrowser( ) ) { m.endComputation(); } 153 | } 154 | 155 | /** 156 | * Load the given language translation file and returns the promise of its loading 157 | */ 158 | function $load( languageToLoad ) { 159 | var deferred = m.deferred(); 160 | if( isBrowser( ) ) { 161 | m.request( { method: "GET", url: $infix() + languageToLoad + $suffix() } ) 162 | .then( function( translations ) { 163 | currentLanguage = languageToLoad || currentLanguage; 164 | storage.set( translations ); 165 | m.endComputation(); 166 | deferred.resolve(); 167 | }, function( error ) { 168 | m.endComputation(); 169 | deferred.reject( error ); 170 | }); 171 | } else if ( isServer( ) ) { 172 | var translations = require( $infix() + languageToLoad + $suffix() ); 173 | currentLanguage = languageToLoad || currentLanguage; 174 | storage.set( translations ); 175 | deferred.resolve(); 176 | } 177 | return deferred.promise; 178 | } 179 | 180 | /** 181 | * Returns the configuration infix option 182 | */ 183 | function $infix() { 184 | return configuration.infix; 185 | } 186 | 187 | /* 188 | * Returns the configuration suffix option 189 | */ 190 | function $suffix() { 191 | return configuration.suffix || ''; 192 | } 193 | 194 | /* 195 | * Returns if we are in a browser mode 196 | */ 197 | function isBrowser( ) { 198 | return typeof window !== "undefined"; 199 | } 200 | 201 | /* 202 | * Returns if we are in a require compliant environment 203 | */ 204 | function isServer( ) { 205 | return typeof module !== "undefined"; 206 | } 207 | 208 | return mx; 209 | 210 | }; 211 | 212 | if ( typeof window !== "undefined" && m ) { 213 | window.mx = mx_factory( m ); 214 | } 215 | if ( typeof module !== "undefined" && module !== null && module.exports ) { 216 | var m = require( 'mithril' ); 217 | module.exports = mx_factory( m ); 218 | } 219 | -------------------------------------------------------------------------------- /dist/mithril-translate.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"dist/mithril-translate.min.js","sources":["dist/mithril-translate.js"],"names":["mx_factory","m","$store","languageToUse","translationsToStore","isBrowser","startComputation","currentLanguage","storage","set","endComputation","$load","languageToLoad","deferred","request","method","url","$infix","$suffix","then","translations","resolve","error","reject","isServer","require","promise","configuration","infix","suffix","window","module","mx","Storage","store","variableRegex","item","get","name","values","result","split","forEach","property","variables","match","key","variable","indexOf","replace","validators","string","TypeError","plainObject","object","translate","arguments","length","secondParameter","thirdParameter","configure","options","use","optionalTranslations","exports"],"mappings":";;AAEA,GAAIA,YAAa,SAAUC,GACvB,YAgJA,SAASC,GAAQC,EAAgBC,GACzBC,KAAiBJ,EAAEK,mBACvBC,EAAkBJ,GAAiBI,EACnCC,EAAQC,IAAKL,GACTC,KAAiBJ,EAAES,iBAM3B,QAASC,GAAOC,GACZ,GAAIC,GAAWZ,EAAEY,UACjB,IAAIR,IACAJ,EAAEa,SAAWC,OAAQ,MAAOC,IAAKC,IAAWL,EAAiBM,MACxDC,KAAM,SAAUC,GACbb,EAAkBK,GAAkBL,EACpCC,EAAQC,IAAKW,GACbnB,EAAES,iBACFG,EAASQ,WACV,SAAUC,GACTrB,EAAES,iBACFG,EAASU,OAAQD,SAEtB,IAAKE,IAAc,CACtB,GAAIJ,GAAeK,QAASR,IAAWL,EAAiBM,IACxDX,GAAkBK,GAAkBL,EACpCC,EAAQC,IAAKW,GACbP,EAASQ,UAEb,MAAOR,GAASa,QAMpB,QAAST,KACL,MAAOU,GAAcC,MAMzB,QAASV,KACL,MAAOS,GAAcE,QAAU,GAMlC,QAASxB,KACN,MAAyB,mBAAXyB,QAMlB,QAASN,KACR,MAAyB,mBAAXO,QAvMf,GAkFIJ,GAKApB,EAvFAyB,KAKAC,EAAU,WACV,GAAIC,MACAC,EAAgB,gBACpB,QAMI1B,IAAK,SAAU2B,GACXF,EAAO3B,GAAoB6B,GAS/BC,IAAK,SAAUC,EAAOC,GAClB,GAAInB,GAAgBc,EAAO3B,GACvBiC,EAASpB,CAMb,IALAkB,EACKG,MAAM,KACNC,QAAS,SAAUC,GAChBH,EAASA,EAAQG,KAErBJ,EAAS,CACT,GAAIK,GAAYJ,EAAOK,MAAOV,EAC9B,KAAK,GAAIW,KAAOP,GACZK,EAAUF,QAAS,SAAUK,GACO,KAA5BA,EAASC,QAASF,KAClBN,EAASA,EAAOS,QAASF,EAAWR,EAAQO,OAK5D,MAAON,MASfU,GAOAC,OAAS,SAASA,GACd,GAAqB,gBAAXA,GACN,KAAM,IAAIC,WAAU,6BAA+BD,EAAS,WAAeA,GAAU,UAEzF,OAAOA,IASXE,YAAc,SAASC,GACnB,GAAqB,gBAAXA,GACN,KAAM,IAAIF,WAAU,8BAAgCE,EAAS,YAAgBA,GAAU,UAE3F,OAAOA,KAiBX9C,EAAU,GAAIyB,EA8GlB,OAzGAD,GAAGuB,UAAY,SAAUnB,GAErB,GADAc,EAAWC,OAAQf,GACfoB,UAAUC,OAAS,EAAK,CACxB,GAAIC,GAAkBF,UAAW,EACjC,IAAyB,IAArBA,UAAUC,OAAe,CACzB,GAAIE,GAAiBH,UAAW,EAChC,OAAOhD,GAAQ6B,IAAKD,EAAO,IAAMuB,EAAiBD,GAC/C,MAA+B,gBAApBA,GACPlD,EAAQ6B,IAAKD,EAAOsB,GAEpBlD,EAAQ6B,IAAKD,EAAO,IAAMsB,GAIrC,MAAOlD,GAAQ6B,IAAKD,IAO5BJ,EAAGuB,UAAUK,UAAY,SAAUC,GAC/BX,EAAWG,YAAaQ,GACxBlC,EAAgBkC,GAMpB7B,EAAGuB,UAAUO,IAAM,SAAU3D,EAAgB4D,GACzC,MAAI5D,IAAiBI,IAAoBJ,GACrC+C,EAAWC,OAAQhD,GACf4D,MACA7D,GAAQC,EAAgB4D,GAEjBpD,EAAOR,IAGXI,GAmERyB,EAOX,IAHuB,mBAAXF,SAA0B7B,IAClC6B,OAAOE,GAAKhC,WAAYC,IAEL,mBAAX8B,SAAqC,OAAXA,QAAmBA,OAAOiC,QAAU,CACtE,GAAI/D,GAAIwB,QAAS,UACjBM,QAAOiC,QAAUhE,WAAYC","sourceRoot":"http://localhost/"} -------------------------------------------------------------------------------- /dist/mithril-translate.min.js: -------------------------------------------------------------------------------- 1 | /*! 2015-11-04 */ 2 | 3 | var mx_factory=function(a){"use strict";function b(b,c){f()&&a.startComputation(),i=b||i,m.set(c),f()&&a.endComputation()}function c(b){var c=a.deferred();if(f())a.request({method:"GET",url:d()+b+e()}).then(function(d){i=b||i,m.set(d),a.endComputation(),c.resolve()},function(b){a.endComputation(),c.reject(b)});else if(g()){var h=require(d()+b+e());i=b||i,m.set(h),c.resolve()}return c.promise}function d(){return h.infix}function e(){return h.suffix||""}function f(){return"undefined"!=typeof window}function g(){return"undefined"!=typeof module}var h,i,j={},k=function(){var a={},b=/\{\{(.*?)\}\}/g;return{set:function(b){a[i]=b},get:function(c,d){var e=a[i],f=e;if(c.split(".").forEach(function(a){f=f[a]}),d){var g=f.match(b);for(var h in d)g.forEach(function(a){-1!==a.indexOf(h)&&(f=f.replace(a,d[h]))})}return f}}},l={string:function(a){if("string"!=typeof a)throw new TypeError("a string is expected, but "+a+" ["+typeof a+"] given");return a},plainObject:function(a){if("object"!=typeof a)throw new TypeError("an object is expected, but "+a+" ["+typeof a+"] given");return a}},m=new k;return j.translate=function(a){if(l.string(a),arguments.length>1){var b=arguments[1];if(3===arguments.length){var c=arguments[2];return m.get(a+"."+c,b)}return"object"==typeof b?m.get(a,b):m.get(a+"."+b)}return m.get(a)},j.translate.configure=function(a){l.plainObject(a),h=a},j.translate.use=function(a,d){return a&&i!==a?(l.string(a),d?void b(a,d):c(a)):i},j};if("undefined"!=typeof window&&m&&(window.mx=mx_factory(m)),"undefined"!=typeof module&&null!==module&&module.exports){var m=require("mithril");module.exports=mx_factory(m)} 4 | //# sourceMappingURL=./mithril-translate.js.map -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2013-07-16 using generator-angular 0.3.0 2 | 'use strict'; 3 | 4 | module.exports = function (grunt) { 5 | 6 | // load all grunt tasks 7 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 8 | 9 | // Configurable paths 10 | var config = { 11 | src: 'src', 12 | dist: 'dist', 13 | test: 'test', 14 | lib: 'test/lib', 15 | i18n: 'test/i18n' 16 | }; 17 | 18 | grunt.initConfig({ 19 | 20 | config: config, 21 | pkg: grunt.file.readJSON('package.json'), 22 | 23 | // Cleaning the release directory 24 | clean: { 25 | dist: { 26 | files: [{ 27 | dot: true, 28 | src: [ 29 | '.tmp', 30 | '<%= config.dist %>/*', 31 | '!<%= config.dist %>/.git*' 32 | ] 33 | }] 34 | } 35 | }, 36 | 37 | // Put files not handled in minification in other tasks here 38 | copy: { 39 | dist: { 40 | files: [{ 41 | expand: true, 42 | dot: true, 43 | cwd: '<%= config.src %>', 44 | dest: '<%= config.dist %>', 45 | src: [ 46 | '*.js' 47 | ] 48 | }] 49 | } 50 | }, 51 | // Minification uglify 52 | uglify: { 53 | dist: { 54 | files: { 55 | '<%= config.dist %>/mithril-translate.min.js': [ 56 | '<%= config.dist %>/mithril-translate.js' 57 | ] 58 | }, 59 | options: { 60 | sourceMap: './mithril-translate.js.map', 61 | sourceMapRoot: 'http://localhost/', 62 | banner: '/*! <%= grunt.template.today("yyyy-mm-dd") %> */\n' 63 | } 64 | } 65 | }, 66 | 67 | // Testing via jasmine 68 | jasmine: { 69 | src: [ 70 | '<%= config.lib %>/mithril/mithril.js' , 71 | '<%= config.lib %>/mithril/mock.js' , 72 | '<%= config.i18n %>/translation.js', 73 | '<%= config.src %>/mithril-translate.js' 74 | ], 75 | options: { 76 | specs : '<%= config.test %>/**/*.js' 77 | } 78 | }, 79 | 80 | // Testing server 81 | connect: { 82 | test: { 83 | options: { 84 | hostname: 'localhost', 85 | port: 9901, 86 | base: '.' 87 | } 88 | } 89 | } 90 | }); 91 | 92 | //run just the tests 93 | grunt.registerTask('test', ['connect:test', 'jasmine' ]); 94 | // Build the package 95 | grunt.registerTask('build', [ 'clean:dist', 'copy:dist', 'uglify' ]); 96 | 97 | }; 98 | 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-translate", 3 | "description": "A i18n library for Mithril.js", 4 | "version": "0.0.1", 5 | "author": { 6 | "name": "Karl Devooght", 7 | "email": "karl.devooght@gmail.com", 8 | "url": "https://github.com/kawan16/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/krampstudio/mithril-translate.git" 13 | }, 14 | "main": "src/mithril-translate.js", 15 | "scripts": { 16 | "test": "grunt test" 17 | }, 18 | "devDependencies": { 19 | "grunt": "^0.4.5", 20 | "grunt-contrib-clean": "~0.4.1", 21 | "grunt-contrib-connect": "^0.10.1", 22 | "grunt-contrib-copy": "~0.4.1", 23 | "grunt-contrib-jasmine": "0.8.2", 24 | "grunt-contrib-uglify": "~0.2.0", 25 | "matchdep": "~0.1.2" 26 | }, 27 | "keywords": [ 28 | "mithril", 29 | "translate", 30 | "i18n" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/mithril-translate.js: -------------------------------------------------------------------------------- 1 | /* global m, mx */ 2 | 3 | var mx_factory = function( m ) { 4 | 'use strict'; 5 | 6 | var mx = {}; 7 | 8 | /** 9 | * Storage constructor 10 | */ 11 | var Storage = function( ) { 12 | var store = {}; 13 | var variableRegex = /\{\{(.*?)\}\}/g; 14 | return { 15 | 16 | /** 17 | * Stores the given translation object in the current language cache 18 | * @param {object} The translation object 19 | */ 20 | set: function( item ) { 21 | store[ currentLanguage ] = item; 22 | }, 23 | 24 | /** 25 | * Returns the translation given the current language, the given name property and the optional 26 | * values replacement in case of template message 27 | * @param {String} The name of translation property 28 | * @param {object} The key/value map of variable / values to replace in the translation content 29 | */ 30 | get: function( name , values ) { 31 | var translations = store[ currentLanguage ]; 32 | var result = translations; 33 | name 34 | .split('.') 35 | .forEach( function( property ) { 36 | result = result[ property ]; 37 | }); 38 | if( values ) { 39 | var variables = result.match( variableRegex ); 40 | for( var key in values ) { 41 | variables.forEach( function( variable ) { 42 | if( variable.indexOf( key ) !== -1 ) { 43 | result = result.replace( variable , values[ key ] ); 44 | } 45 | }) 46 | } 47 | } 48 | return result; 49 | } 50 | } 51 | }; 52 | 53 | 54 | /* 55 | * Validators for router function 56 | */ 57 | var validators = { 58 | /** 59 | * Check whether the given parameter is a string 60 | * @param {String} string 61 | * @returns {String} value 62 | * @throws {TypeError} for non strings 63 | */ 64 | string : function(string){ 65 | if(typeof string !== 'string'){ 66 | throw new TypeError('a string is expected, but ' + string + ' [' + (typeof string) + '] given'); 67 | } 68 | return string; 69 | }, 70 | 71 | /** 72 | * Check whether the given parameter is a plain object (array and functions aren't accepted) 73 | * @param {Object} object 74 | * @returns {Object} object 75 | * @throws {TypeError} for non object 76 | */ 77 | plainObject : function(object){ 78 | if(typeof object !== 'object' ){ 79 | throw new TypeError('an object is expected, but ' + object + ' [' + (typeof object) + '] given'); 80 | } 81 | return object; 82 | } 83 | }; 84 | 85 | /** 86 | * The module configuration 87 | */ 88 | var configuration; 89 | 90 | /** 91 | * The current language 92 | */ 93 | var currentLanguage; 94 | 95 | /** 96 | * The translation storage 97 | */ 98 | var storage = new Storage(); 99 | 100 | /** 101 | * Returns the translation of a given item name with optional variable/value substitution 102 | */ 103 | mx.translate = function( item ) { 104 | validators.string( item ); 105 | if( arguments.length > 1 ) { 106 | var secondParameter = arguments[ 1 ]; 107 | if( arguments.length === 3 ) { 108 | var thirdParameter = arguments[ 2 ]; 109 | return storage.get( item + '.' + thirdParameter , secondParameter ); 110 | } else if( typeof secondParameter === 'object' ) { 111 | return storage.get( item , secondParameter ); 112 | } else { 113 | return storage.get( item + '.' + secondParameter ); 114 | } 115 | } 116 | else { 117 | return storage.get( item ); 118 | } 119 | }; 120 | 121 | /** 122 | * Loads the translations files for the given folder 123 | */ 124 | mx.translate.configure = function( options ) { 125 | validators.plainObject( options ); 126 | configuration = options; 127 | }; 128 | 129 | /** 130 | * Get / Set the language 131 | */ 132 | mx.translate.use = function( languageToUse , optionalTranslations ) { 133 | if( languageToUse && currentLanguage !== languageToUse ) { 134 | validators.string( languageToUse ); 135 | if( optionalTranslations ) { 136 | $store( languageToUse , optionalTranslations ); 137 | } else { 138 | return $load( languageToUse ); 139 | } 140 | } else { 141 | return currentLanguage; 142 | } 143 | }; 144 | 145 | /** 146 | * Stores static translations for a given language 147 | */ 148 | function $store( languageToUse , translationsToStore ) { 149 | if( isBrowser( ) ) { m.startComputation(); } 150 | currentLanguage = languageToUse || currentLanguage; 151 | storage.set( translationsToStore ); 152 | if( isBrowser( ) ) { m.endComputation(); } 153 | } 154 | 155 | /** 156 | * Load the given language translation file and returns the promise of its loading 157 | */ 158 | function $load( languageToLoad ) { 159 | var deferred = m.deferred(); 160 | if( isBrowser( ) ) { 161 | m.request( { method: "GET", url: $infix() + languageToLoad + $suffix() } ) 162 | .then( function( translations ) { 163 | currentLanguage = languageToLoad || currentLanguage; 164 | storage.set( translations ); 165 | m.endComputation(); 166 | deferred.resolve(); 167 | }, function( error ) { 168 | m.endComputation(); 169 | deferred.reject( error ); 170 | }); 171 | } else if ( isServer( ) ) { 172 | var translations = require( $infix() + languageToLoad + $suffix() ); 173 | currentLanguage = languageToLoad || currentLanguage; 174 | storage.set( translations ); 175 | deferred.resolve(); 176 | } 177 | return deferred.promise; 178 | } 179 | 180 | /** 181 | * Returns the configuration infix option 182 | */ 183 | function $infix() { 184 | return configuration.infix; 185 | } 186 | 187 | /* 188 | * Returns the configuration suffix option 189 | */ 190 | function $suffix() { 191 | return configuration.suffix || ''; 192 | } 193 | 194 | /* 195 | * Returns if we are in a browser mode 196 | */ 197 | function isBrowser( ) { 198 | return typeof window !== "undefined"; 199 | } 200 | 201 | /* 202 | * Returns if we are in a require compliant environment 203 | */ 204 | function isServer( ) { 205 | return typeof module !== "undefined"; 206 | } 207 | 208 | return mx; 209 | 210 | }; 211 | 212 | if ( typeof window !== "undefined" && m ) { 213 | window.mx = mx_factory( m ); 214 | } 215 | if ( typeof module !== "undefined" && module !== null && module.exports ) { 216 | var m = require( 'mithril' ); 217 | module.exports = mx_factory( m ); 218 | } 219 | -------------------------------------------------------------------------------- /test/i18n/translations.js: -------------------------------------------------------------------------------- 1 | 2 | var i18n = i18n || {}; 3 | 4 | i18n.en = { 5 | title: "The Little prince", 6 | sample: "In the book it said: \"Boa constrictors swallow their prey whole, without chewing it. After that they are not able to move, and they sleep through the six months that they need for digestion.\" I pondered deeply, then, over the adventures of the jungle. And after some work with a colored pencil I succeeded in making my first drawing. My Drawing Number One. [...]", 7 | chapterOne: { 8 | title: "Chapter one" 9 | }, 10 | message: 'This is your message: "{{ message }}"', 11 | viewing : { 12 | '0' : 'Nobody is viewing', 13 | '1' : '{{ person1 }} is viewing', 14 | '2' : '{{ person1 }} and {{ person2 }} are viewing', 15 | 'other': '{{ count }} persons are viewing' 16 | } 17 | } 18 | 19 | i18n.de = { 20 | title: "Kapitel eins", 21 | sample: "In dem Buche hieß es: \"Die Boas verschlingen ihre Beute als Ganzes, ohne sie zu zerbeißen. Daraufhin können sie sich nicht mehr rühren und schlafen sechs Monate, um zu verdauen.\" Ich habe damals viel über die Abenteuer des Dschungels nachgedacht, und ich vollendete mit einem Farbstift meine erste Zeichnung. Meine Zeichnung Nr. 1. [...]", 22 | chapterOne: { 23 | title: "Chapter one" 24 | }, 25 | message: 'C\'est votre message: "{{ message }}"', 26 | viewing : { 27 | '0' : 'Nobody is viewing', 28 | '1' : '{{ person1 }} is viewing', 29 | '2' : '{{ person1 }} , {{ person2 }} are viewing', 30 | 'other': '{{ count }} persons are viewing' 31 | } 32 | } 33 | 34 | 35 | i18n.fr = { 36 | title: "Le Petit prince", 37 | sample: "On disait dans le livre: \"Les serpents boas avalent leur proie tout entière, sans la mâcher. Ensuite ils ne peuvent plus bouger et ils dorment pendant les six mois de leur digestion\".J'ai alors beaucoup réfléchi sur les aventures de la jungle et, à mon tour, j'ai réussi, avec un crayon de couleur, à tracer mon premier dessin. Mon dessin numéro 1. [...]", 38 | chapterOne: { 39 | title: "Chapitre un" 40 | }, 41 | message: 'This is your message: "{{ message }}"', 42 | viewing : { 43 | '0' : 'Personne ne regarde', 44 | '1' : '{{ person1 }} regarde', 45 | '2' : '{{ person1 }} , {{ person2 }} regardent', 46 | 'other': '{{ count }} personnes regardent' 47 | } 48 | } 49 | 50 | /* Mock Http Request */ 51 | m.request = function( options ) { 52 | var deferred = m.deferred(); 53 | if( options.url === '/i18n/en.json' ) { 54 | deferred.resolve( i18n.en ); 55 | } else if ( options.url === '/i18n/de.json' ) { 56 | deferred.resolve( i18n.de ); 57 | } else if ( options.url === '/i18n/fr.json' ) { 58 | deferred.resolve( i18n.fr ); 59 | } 60 | return deferred.promise; 61 | } 62 | -------------------------------------------------------------------------------- /test/lib/mithril/mithril.js: -------------------------------------------------------------------------------- 1 | var m = (function app(window, undefined) { 2 | var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; 3 | var type = {}.toString; 4 | var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; 5 | var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; 6 | 7 | // caching commonly used variables 8 | var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; 9 | 10 | // self invoking function needed because of the way mocks work 11 | function initialize(window){ 12 | $document = window.document; 13 | $location = window.location; 14 | $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; 15 | $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; 16 | } 17 | 18 | initialize(window); 19 | 20 | 21 | /** 22 | * @typedef {String} Tag 23 | * A string that looks like -> div.classname#id[param=one][param2=two] 24 | * Which describes a DOM node 25 | */ 26 | 27 | /** 28 | * 29 | * @param {Tag} The DOM node tag 30 | * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs 31 | * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) 32 | * 33 | */ 34 | function m() { 35 | var args = [].slice.call(arguments); 36 | var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]); 37 | var attrs = hasAttrs ? args[1] : {}; 38 | var classAttrName = "class" in attrs ? "class" : "className"; 39 | var cell = {tag: "div", attrs: {}}; 40 | var match, classes = []; 41 | if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") 42 | while (match = parser.exec(args[0])) { 43 | if (match[1] === "" && match[2]) cell.tag = match[2]; 44 | else if (match[1] === "#") cell.attrs.id = match[2]; 45 | else if (match[1] === ".") classes.push(match[2]); 46 | else if (match[3][0] === "[") { 47 | var pair = attrParser.exec(match[3]); 48 | cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) 49 | } 50 | } 51 | if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); 52 | 53 | 54 | var children = hasAttrs ? args.slice(2) : args.slice(1); 55 | if (children.length === 1 && type.call(children[0]) === ARRAY) { 56 | cell.children = children[0] 57 | } 58 | else { 59 | cell.children = children 60 | } 61 | 62 | for (var attrName in attrs) { 63 | if (attrName === classAttrName) { 64 | var className = cell.attrs[attrName] 65 | cell.attrs[attrName] = (className && attrs[attrName] ? className + " " : className || "") + attrs[attrName]; 66 | } 67 | else cell.attrs[attrName] = attrs[attrName] 68 | } 69 | return cell 70 | } 71 | function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { 72 | //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` 73 | //the diff algorithm can be summarized as this: 74 | //1 - compare `data` and `cached` 75 | //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is 76 | //3 - recursively apply this algorithm for every array and for the children of every virtual element 77 | 78 | //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: 79 | //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element 80 | //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` 81 | //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) 82 | //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node 83 | 84 | //`parentElement` is a DOM element used for W3C DOM API calls 85 | //`parentTag` is only used for handling a corner case for textarea values 86 | //`parentCache` is used to remove nodes in some multi-node cases 87 | //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable 88 | //`data` and `cached` are, respectively, the new and old nodes being diffed 89 | //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) 90 | //`editable` is a flag that indicates whether an ancestor is contenteditable 91 | //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor 92 | //`configs` is a list of config functions to run after the topmost `build` call finishes running 93 | 94 | //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings 95 | //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} 96 | //- it simplifies diffing code 97 | //data.toString() is null if data is the return value of Console.log in Firefox 98 | try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""} 99 | if (data.subtree === "retain") return cached; 100 | var cachedType = type.call(cached), dataType = type.call(data); 101 | if (cached == null || cachedType !== dataType) { 102 | if (cached != null) { 103 | if (parentCache && parentCache.nodes) { 104 | var offset = index - parentIndex; 105 | var end = offset + (dataType === ARRAY ? data : cached.nodes).length; 106 | clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) 107 | } 108 | else if (cached.nodes) clear(cached.nodes, cached) 109 | } 110 | cached = new data.constructor; 111 | if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) 112 | cached.nodes = [] 113 | } 114 | 115 | if (dataType === ARRAY) { 116 | //recursively flatten array 117 | for (var i = 0, len = data.length; i < len; i++) { 118 | if (type.call(data[i]) === ARRAY) { 119 | data = data.concat.apply([], data); 120 | i-- //check current index again and flatten until there are no more nested arrays at that index 121 | len = data.length 122 | } 123 | } 124 | 125 | var nodes = [], intact = cached.length === data.length, subArrayCount = 0; 126 | 127 | //keys algorithm: sort elements without recreating them if keys are present 128 | //1) create a map of all existing keys, and mark all for deletion 129 | //2) add new keys to map and mark them for addition 130 | //3) if key exists in new list, change action from deletion to a move 131 | //4) for each key, handle its corresponding action as marked in previous steps 132 | var DELETION = 1, INSERTION = 2 , MOVE = 3; 133 | var existing = {}, unkeyed = [], shouldMaintainIdentities = false; 134 | for (var i = 0; i < cached.length; i++) { 135 | if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { 136 | shouldMaintainIdentities = true; 137 | existing[cached[i].attrs.key] = {action: DELETION, index: i} 138 | } 139 | } 140 | 141 | var guid = 0 142 | for (var i = 0, len = data.length; i < len; i++) { 143 | if (data[i] && data[i].attrs && data[i].attrs.key != null) { 144 | for (var j = 0, len = data.length; j < len; j++) { 145 | if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++ 146 | } 147 | break 148 | } 149 | } 150 | 151 | if (shouldMaintainIdentities) { 152 | var keysDiffer = false 153 | if (data.length != cached.length) keysDiffer = true 154 | else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { 155 | if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { 156 | keysDiffer = true 157 | break 158 | } 159 | } 160 | 161 | if (keysDiffer) { 162 | for (var i = 0, len = data.length; i < len; i++) { 163 | if (data[i] && data[i].attrs) { 164 | if (data[i].attrs.key != null) { 165 | var key = data[i].attrs.key; 166 | if (!existing[key]) existing[key] = {action: INSERTION, index: i}; 167 | else existing[key] = { 168 | action: MOVE, 169 | index: i, 170 | from: existing[key].index, 171 | element: cached.nodes[existing[key].index] || $document.createElement("div") 172 | } 173 | } 174 | } 175 | } 176 | var actions = [] 177 | for (var prop in existing) actions.push(existing[prop]) 178 | var changes = actions.sort(sortChanges); 179 | var newCached = new Array(cached.length) 180 | newCached.nodes = cached.nodes.slice() 181 | 182 | for (var i = 0, change; change = changes[i]; i++) { 183 | if (change.action === DELETION) { 184 | clear(cached[change.index].nodes, cached[change.index]); 185 | newCached.splice(change.index, 1) 186 | } 187 | if (change.action === INSERTION) { 188 | var dummy = $document.createElement("div"); 189 | dummy.key = data[change.index].attrs.key; 190 | parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); 191 | newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) 192 | newCached.nodes[change.index] = dummy 193 | } 194 | 195 | if (change.action === MOVE) { 196 | if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { 197 | parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) 198 | } 199 | newCached[change.index] = cached[change.from] 200 | newCached.nodes[change.index] = change.element 201 | } 202 | } 203 | cached = newCached; 204 | } 205 | } 206 | //end key algorithm 207 | 208 | for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { 209 | //diff each item in the array 210 | var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); 211 | if (item === undefined) continue; 212 | if (!item.nodes.intact) intact = false; 213 | if (item.$trusted) { 214 | //fix offset of next element if item was a trusted string w/ more than one html element 215 | //the first clause in the regexp matches elements 216 | //the second clause (after the pipe) matches text nodes 217 | subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length 218 | } 219 | else subArrayCount += type.call(item) === ARRAY ? item.length : 1; 220 | cached[cacheCount++] = item 221 | } 222 | if (!intact) { 223 | //diff the array itself 224 | 225 | //update the list of DOM nodes by collecting the nodes from each item 226 | for (var i = 0, len = data.length; i < len; i++) { 227 | if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) 228 | } 229 | //remove items from the end of the array if the new array is shorter than the old one 230 | //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program 231 | for (var i = 0, node; node = cached.nodes[i]; i++) { 232 | if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) 233 | } 234 | if (data.length < cached.length) cached.length = data.length; 235 | cached.nodes = nodes 236 | } 237 | } 238 | else if (data != null && dataType === OBJECT) { 239 | if (!data.attrs) data.attrs = {}; 240 | if (!cached.attrs) cached.attrs = {}; 241 | 242 | var dataAttrKeys = Object.keys(data.attrs) 243 | var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) 244 | //if an element is different enough from the one in cache, recreate it 245 | if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.retain !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { 246 | if (cached.nodes.length) clear(cached.nodes); 247 | if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() 248 | } 249 | if (type.call(data.tag) != STRING) return; 250 | 251 | var node, isNew = cached.nodes.length === 0; 252 | if (data.attrs.xmlns) namespace = data.attrs.xmlns; 253 | else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; 254 | else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; 255 | if (isNew) { 256 | if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); 257 | else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); 258 | cached = { 259 | tag: data.tag, 260 | //set attributes first, then create children 261 | attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, 262 | children: data.children != null && data.children.length > 0 ? 263 | build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : 264 | data.children, 265 | nodes: [node] 266 | }; 267 | if (cached.children && !cached.children.nodes) cached.children.nodes = []; 268 | //edge case: setting value on