├── .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 [](http://gruntjs.com/)
2 | =================
3 |
4 | [](https://travis-ci.org/kawan16/mithril-translate) [](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