├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── bower.json ├── i1-8n.tag ├── index.js ├── package.json ├── riot-i18n.js ├── riot-i18n.min.js ├── scripts └── pack.js └── test ├── riot-2.3.0.html ├── riot-3.0.7.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # IDE metadata 31 | .idea 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6.1" 5 | - "6.0" 6 | - "5.0" 7 | - "4.1" 8 | - "4.0" 9 | 10 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-anycode": { 3 | "name": "riot-i18n", 4 | "description": "Riot JS internationalization tag and mixin", 5 | "github": true 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | _ ____ _ 2 | / \ _ __ _ _ / ___|___ __| | ___ 3 | / _ \ | '_ \| | | | | | / _ \ / _` |/ _ \ 4 | / ___ \| | | | |_| | | |__| (_) | (_| | __/ 5 | /_/ \_\_| |_|\__, | \____\___/ \__,_|\___| 6 | |___/ 7 | 8 | Copyright (c) 2015-2016, Any Code 9 | 10 | Permission to use, copy, modify, and/or distribute this software for any 11 | purpose with or without fee is hereby granted, provided that the above 12 | copyright notice and this permission notice appear in all copies. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # riot-i18n 2 | 3 | [![Build Status](https://travis-ci.org/any-code/riot-i18n.svg?branch=master)](https://travis-ci.org/any-code/riot-i18n) 4 | 5 | > Riot JS internationalization tag and mixin 6 | 7 | ## Getting Started 8 | 9 | ### 1. Installation 10 | 11 | ``` bash 12 | npm install riot-i18n 13 | ``` 14 | 15 | ### 2. Examples 16 | 17 | include with module loader 18 | ``` javascript 19 | var i18n = require('riot-i18n'); 20 | 21 | i18n.dictionary({ 22 | "zh": { 23 | "Hello": "您好", 24 | "I love you": "我爱你" 25 | }, 26 | "jp": { 27 | "Hello": "こんにちは", 28 | "I love you": "わたしは、あなたを愛しています" 29 | } 30 | }) 31 | ``` 32 | 33 | or via script tag 34 | ``` html 35 | 36 | 37 | ``` 38 | 39 | The library register's the 'i1-8n' tag with riot. 40 | ``` html 41 |
42 | Hello
43 | I love you 44 |
45 | ``` 46 | 47 | and provides a 'localise' method for translating outside of a tag 48 | ``` javascript 49 | i18n.setLanguage('zh') 50 | i18n.localise('Hello') // -> 您好 51 | ``` 52 | 53 | setting language can be achieved using i18n.setLanguage('lang') or triggered using a riot observable 54 | ``` html 55 | 56 | 61 | 62 | riot.mixin('i18n') 63 | 64 | this.i18n.setLanguage('fr') 65 | this.i18n.localise('Hello') // -> Hello 66 | 67 | this.onClick = function(e) { 68 | this.i18n.trigger('lang', e.target.innerHTML) 69 | } 70 | 71 | this.i18n.trigger('lang', 'jp') 72 | this.i18n.localise('Hello') // -> こんにちは 73 | 74 | ``` 75 | 76 | If no dictionary language substitute is available the default will always be used 77 | 78 | localise method substitution object 79 | ```javascript 80 | 81 | this.i18n.localise("Hello {user.name}!", { 82 | user: { 83 | name: "Goodman" 84 | } 85 | }) // --> Hello Goodman! 86 | 87 | ``` 88 | 89 | nested properties 90 | ```javascript 91 | i18n.dictionary({ 92 | "en": { 93 | "user": { 94 | "name": "User Name" 95 | } 96 | }) 97 | 98 | this.i18n.localise("user.name") // --> User Name 99 | ``` 100 | 101 | 102 | 103 | ## Copyright and license 104 | Copyright (c) 2015-2017 [Anycode](https://anycode.io/ "Anycode") 105 | 106 | Permission to use, copy, modify, and/or distribute this software for any 107 | purpose with or without fee is hereby granted, provided that the above 108 | copyright notice and this permission notice appear in all copies. 109 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riot-i18n", 3 | "description": "Riot JS internationalization tag and mixin", 4 | "main": "riot-i18n.min.js", 5 | "authors": [ 6 | "Lee Tagg " 7 | ], 8 | "license": "ISC", 9 | "keywords": [ 10 | "riot-i18n", 11 | "riot", 12 | "riotjs", 13 | "internationalization", 14 | "internationalisation", 15 | "localisation", 16 | "localization", 17 | "i18n", 18 | "L10n", 19 | "g11n" 20 | ], 21 | "dependencies": { 22 | "riot": "^2.3.11" 23 | }, 24 | "homepage": "https://github.com/any-code/riot-i18n", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests", 31 | "package.json" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /i1-8n.tag: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['exports', 'riot'], function (exports, riot) { 5 | factory((root.i18n = exports), riot); 6 | }); 7 | } else if (typeof exports === 'object') { 8 | // CommonJS 9 | factory(exports, require('riot')); 10 | } else { 11 | // Browser globals 12 | factory((root.i18n = {}), root.riot); 13 | } 14 | }(this, function (exports, riot) { 15 | 16 | var DEFAULT_LANG = 'en'; 17 | 18 | function I18n() { 19 | this._entities = {} 20 | this._default = DEFAULT_LANG 21 | this._language = this._default 22 | var obs = riot.observable() 23 | this.on = obs.on 24 | this.off = obs.off 25 | this.trigger = obs.trigger 26 | this.on('lang', this.setLanguage) 27 | } 28 | 29 | I18n.prototype.dictionary = function(dict) { 30 | this._entities = dict; 31 | } 32 | 33 | I18n.prototype.defaultLanguage = function(lang) { 34 | this._default = lang; 35 | } 36 | 37 | I18n.prototype.localise = function(key, data) { 38 | var substitute, locale; 39 | 40 | function flatten(n, f, d, k) { 41 | k = k || "", f = f || {}, d = d || 0 42 | var nObj = n && !n.length, 43 | nKeys = nObj ? Object.keys(n).length : 0; 44 | 45 | if (nObj && nKeys > 0) { 46 | var i; 47 | for (i in n) { 48 | if (k.split('.').length > d) { k = k.split('.').splice(0, d).join('.') } 49 | k = (d == 0) ? i : k + "." + i, f = flatten(n[i], f, d + 1, k) 50 | } 51 | } else f[k] = n; 52 | return f; 53 | } 54 | 55 | if (!this._entities[this._language]) { 56 | // When a language is not provided, 57 | // treat the key as substitution 58 | substitute = key; 59 | } 60 | 61 | if (!substitute) { 62 | locale = flatten(this._entities[this._language]); 63 | 64 | if (!locale[key]) { 65 | // When a translation is not 66 | // provided, just return the original text. 67 | substitute = key 68 | } else { 69 | // return the language substitue for the original text 70 | substitute = locale[key]; 71 | } 72 | } 73 | 74 | if (data) { 75 | var _data = flatten(data), 76 | _key; 77 | for (_key in _data) { 78 | substitute = substitute.replace(new RegExp("{" + _key + "}", "g"), _data[_key]); 79 | } 80 | } 81 | 82 | return substitute; 83 | } 84 | 85 | I18n.prototype.setLanguage = function(lang) { 86 | this._language = lang || this._default 87 | this.trigger('update') 88 | } 89 | 90 | I18n.prototype.getLanguage = function() { 91 | return this._language 92 | } 93 | 94 | var i18n = new I18n(), 95 | property; 96 | 97 | for (property in i18n) { 98 | exports[property] = i18n[property]; 99 | } 100 | riot.mixin('i18n', { i18n: exports }) 101 | 102 | // 103 | // 104 | // BEGIN RIOT TAGS 105 | 106 | // END RIOT TAGS 107 | // 108 | // 109 | 110 | })); 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riot-i18n", 3 | "version": "0.2.1", 4 | "description": "Riot JS internationalization tag and mixin", 5 | "main": "riot-i18n.min.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "nodeunit", 11 | "pub-pack": "node scripts/pack.js index.js i1-8n.tag riot-i18n.js", 12 | "pub-minify": "uglifyjs riot-i18n.js --compress --screw-ie8 --mangle > riot-i18n.min.js", 13 | "pub-build": "npm run pub-pack && npm run pub-minify && npm run test", 14 | "pub-npm": "npm version patch && npm publish" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/any-code/riot-i18n.git" 19 | }, 20 | "keywords": [ 21 | "riot-i18n", 22 | "riot", 23 | "riotjs", 24 | "internationalization", 25 | "internationalisation", 26 | "localisation", 27 | "localization", 28 | "i18n", 29 | "L10n", 30 | "g11n" 31 | ], 32 | "author": "Lee Tagg ", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/any-code/riot-i18n/issues" 36 | }, 37 | "homepage": "https://github.com/any-code/riot-i18n#readme", 38 | "devDependencies": { 39 | "nodeunit": "^0.9.1", 40 | "riot": "^3.4.2" 41 | }, 42 | "peerDependencies": { 43 | "riot": "^2.3.11 || ^3.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /riot-i18n.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['exports', 'riot'], function (exports, riot) { 5 | factory((root.i18n = exports), riot); 6 | }); 7 | } else if (typeof exports === 'object') { 8 | // CommonJS 9 | factory(exports, require('riot')); 10 | } else { 11 | // Browser globals 12 | factory((root.i18n = {}), root.riot); 13 | } 14 | }(this, function (exports, riot) { 15 | 16 | var DEFAULT_LANG = 'en'; 17 | 18 | function I18n() { 19 | this._entities = {} 20 | this._default = DEFAULT_LANG 21 | this._language = this._default 22 | var obs = riot.observable() 23 | this.on = obs.on 24 | this.off = obs.off 25 | this.trigger = obs.trigger 26 | this.on('lang', this.setLanguage) 27 | } 28 | 29 | I18n.prototype.dictionary = function(dict) { 30 | this._entities = dict; 31 | } 32 | 33 | I18n.prototype.defaultLanguage = function(lang) { 34 | this._default = lang; 35 | } 36 | 37 | I18n.prototype.localise = function(key, data) { 38 | var substitute, locale; 39 | 40 | function flatten(n, f, d, k) { 41 | k = k || "", f = f || {}, d = d || 0 42 | var nObj = n && !n.length, 43 | nKeys = nObj ? Object.keys(n).length : 0; 44 | 45 | if (nObj && nKeys > 0) { 46 | var i; 47 | for (i in n) { 48 | if (k.split('.').length > d) { k = k.split('.').splice(0, d).join('.') } 49 | k = (d == 0) ? i : k + "." + i, f = flatten(n[i], f, d + 1, k) 50 | } 51 | } else f[k] = n; 52 | return f; 53 | } 54 | 55 | if (!this._entities[this._language]) { 56 | // When a language is not provided, 57 | // treat the key as substitution 58 | substitute = key; 59 | } 60 | 61 | if (!substitute) { 62 | locale = flatten(this._entities[this._language]); 63 | 64 | if (!locale[key]) { 65 | // When a translation is not 66 | // provided, just return the original text. 67 | substitute = key 68 | } else { 69 | // return the language substitue for the original text 70 | substitute = locale[key]; 71 | } 72 | } 73 | 74 | if (data) { 75 | var _data = flatten(data), 76 | _key; 77 | for (_key in _data) { 78 | substitute = substitute.replace(new RegExp("{" + _key + "}", "g"), _data[_key]); 79 | } 80 | } 81 | 82 | return substitute; 83 | } 84 | 85 | I18n.prototype.setLanguage = function(lang) { 86 | this._language = lang || this._default 87 | this.trigger('update') 88 | } 89 | 90 | I18n.prototype.getLanguage = function() { 91 | return this._language 92 | } 93 | 94 | var i18n = new I18n(), 95 | property; 96 | 97 | for (property in i18n) { 98 | exports[property] = i18n[property]; 99 | } 100 | riot.mixin('i18n', { i18n: exports }) 101 | 102 | // 103 | // 104 | //BEGIN RIOT TAGS 105 | riot.tag2('i1-8n', ' ', 'i1-8n,[riot-tag="i1-8n"],[data-is="i1-8n"]{ display: inline-block; } i1-8n .original,[riot-tag="i1-8n"] .original,[data-is="i1-8n"] .original{ display: none; }', '', function(opts) { 106 | this.mixin('i18n') 107 | 108 | this.i18n.on('update', function() { 109 | this.update() 110 | }.bind(this)) 111 | 112 | this.on('mount', function() { 113 | this.hasRefs = this.refs != undefined 114 | this.localise() 115 | }) 116 | 117 | this.on('update', function() { 118 | this.localise() 119 | }) 120 | 121 | this.localise = function() { 122 | var refs = this.hasRefs ? this.refs : this; 123 | refs.localised.innerHTML = this.i18n.localise(refs.original.innerHTML); 124 | } 125 | 126 | }); 127 | //END RIOT TAGS 128 | // 129 | // 130 | 131 | })); 132 | -------------------------------------------------------------------------------- /riot-i18n.min.js: -------------------------------------------------------------------------------- 1 | !function(i,t){"function"==typeof define&&define.amd?define(["exports","riot"],function(n,e){t(i.i18n=n,e)}):"object"==typeof exports?t(exports,require("riot")):t(i.i18n={},i.riot)}(this,function(i,t){function n(){this._entities={},this._default=e,this._language=this._default;var i=t.observable();this.on=i.on,this.off=i.off,this.trigger=i.trigger,this.on("lang",this.setLanguage)}var e="en";n.prototype.dictionary=function(i){this._entities=i},n.prototype.defaultLanguage=function(i){this._default=i},n.prototype.localise=function(i,t){function n(i,t,e,s){s=s||"",t=t||{},e=e||0;var a=i&&!i.length,o=a?Object.keys(i).length:0;if(a&&o>0){var r;for(r in i)s.split(".").length>e&&(s=s.split(".").splice(0,e).join(".")),s=0==e?r:s+"."+r,t=n(i[r],t,e+1,s)}else t[s]=i;return t}var e,s;if(this._entities[this._language]||(e=i),e||(s=n(this._entities[this._language]),e=s[i]?s[i]:i),t){var a,o=n(t);for(a in o)e=e.replace(new RegExp("{"+a+"}","g"),o[a])}return e},n.prototype.setLanguage=function(i){this._language=i||this._default,this.trigger("update")},n.prototype.getLanguage=function(){return this._language};var s,a=new n;for(s in a)i[s]=a[s];t.mixin("i18n",{i18n:i}),t.tag2("i1-8n",' ','i1-8n,[riot-tag="i1-8n"],[data-is="i1-8n"]{ display: inline-block; } i1-8n .original,[riot-tag="i1-8n"] .original,[data-is="i1-8n"] .original{ display: none; }',"",function(){this.mixin("i18n"),this.i18n.on("update",function(){this.update()}.bind(this)),this.on("mount",function(){this.hasRefs=void 0!=this.refs,this.localise()}),this.on("update",function(){this.localise()}),this.localise=function(){var i=this.hasRefs?this.refs:this;i.localised.innerHTML=this.i18n.localise(i.original.innerHTML)}})}); -------------------------------------------------------------------------------- /scripts/pack.js: -------------------------------------------------------------------------------- 1 | if (!process.argv[3]) { 2 | console.log("usage: node pack.js {template} {tag} {output}"); 3 | return; 4 | } 5 | 6 | var riot = require('riot'), 7 | fs = require('fs'), 8 | file = fs.readFileSync(process.argv[2], 'utf-8'), 9 | js = riot.compile(fs.readFileSync(process.argv[3], 'utf-8')) 10 | 11 | file = file.replace(/\/\/\sBEGIN\sRIOT\sTAGS[\s\S]+\/\/\sEND\sRIOT\sTAGS/, "//BEGIN RIOT TAGS\n" + 12 | js + " //END RIOT TAGS"); 13 | fs.writeFile(process.argv[4], file); 14 | console.log('packed: ' + process.argv[4]); 15 | 16 | -------------------------------------------------------------------------------- /test/riot-2.3.0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | riot-i18n test 6 | 7 | 8 | HelloI LOVE YOUI love youHellohello 9 | HELLOI love you 10 | 11 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/riot-3.0.7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | riot-i18n test 6 | 7 | 8 | HelloI LOVE YOUI love youHellohello 9 | HELLOI love you 10 | 11 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var testable = require('../riot-i18n'); 2 | 3 | exports.setUp = function(callback) { 4 | testable.off('*'); 5 | 6 | testable.dictionary({ 7 | "te": { 8 | "nested": { 9 | "property": { 10 | "deep": { 11 | "deep": { 12 | "deep": { 13 | "deep": { 14 | "deep": "cave dweller" 15 | } 16 | } 17 | } 18 | } 19 | } 20 | }, 21 | "create":{ 22 | "labels": { 23 | "chat:": { 24 | "test": "au revoir" 25 | } 26 | } 27 | } 28 | }, 29 | "zh": { 30 | "Hello": "您好", 31 | "I love you": "我爱你" 32 | }, 33 | "jp": { 34 | "Hello": "こんにちは", 35 | "I love you": "わたしは、あなたを愛しています" 36 | } 37 | }) 38 | callback(); 39 | } 40 | 41 | exports.tearDown = function(callback) { 42 | callback(); 43 | } 44 | 45 | exports.testGetLanguage = function(test) { 46 | test.equals(testable.getLanguage(), 'en', "unexpected value returned"); 47 | test.done(); 48 | } 49 | 50 | exports.testSetLocalise = function(test) { 51 | testable.on('update', function() { 52 | test.equals(testable.localise('Hello'), 'こんにちは', "unexpected value returned") 53 | test.done(); 54 | 55 | }.bind(this)) 56 | testable.setLanguage('jp'); 57 | } 58 | 59 | exports.testSetLocaliseAtOdds = function(test) { 60 | testable.on('update', function() { 61 | testable.off('update'); 62 | test.equals(testable.localise('Hello'), 'Hello', "unexpected value returned") 63 | test.done(); 64 | }.bind(this)) 65 | testable.setLanguage('fr'); 66 | } 67 | 68 | exports.testNestedProperty = function(test) { 69 | testable.on('update', function() { 70 | testable.off('update'); 71 | test.equals(testable.localise("nested.property.deep.deep.deep.deep.deep"), "cave dweller", "Unexpected value returned"); 72 | test.done(); 73 | }.bind(this)) 74 | testable.setLanguage('te'); 75 | } 76 | 77 | exports.testAnotherNestedProperty = function(test) { 78 | testable.on('update', function() { 79 | testable.off('update'); 80 | test.equals(testable.localise("create.labels.chat:.test"), "au revoir", "Unexpected value returned"); 81 | test.done(); 82 | }.bind(this)) 83 | testable.setLanguage('te'); 84 | } 85 | 86 | 87 | exports.testSetLocaliseMultiples = function(test) { 88 | var triggered = 0; 89 | testable.on('update', function() { 90 | triggered++; 91 | test.ok(triggered > 0, 'update not triggered'); 92 | if (triggered === 4) { 93 | test.done(); 94 | } 95 | }.bind(this)) 96 | testable.setLanguage('en'); 97 | testable.setLanguage('fr'); 98 | testable.setLanguage('jp'); 99 | testable.setLanguage('zh'); 100 | } 101 | 102 | exports.testSetLocaliseWithSubstitutions = function(test) { 103 | var obj = { 104 | data: { 105 | user: { 106 | name: "Girl Boy", 107 | email: "girl.boy@example.com" 108 | } 109 | } 110 | } 111 | 112 | test.equals(testable.localise("Hello {data.user.name}, is your email address really {data.user.email}", obj), 'Hello Girl Boy, is your email address really girl.boy@example.com', "unexpected value returned"); 113 | test.done(); 114 | } 115 | --------------------------------------------------------------------------------