├── .gitignore ├── demo ├── assets │ ├── bg.jpg │ └── style.css ├── child.vue ├── main.js └── index.html ├── .babelrc ├── .npmignore ├── .editorconfig ├── test ├── index.js ├── karma.conf.js └── specs │ └── translator.spec.js ├── src ├── translator.js └── index.js ├── dist ├── translator.js └── index.js ├── webpack.config.js ├── LICENSE ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /demo/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pespantelis/vue-localizer/HEAD/demo/assets/bg.jpg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .babelrc 3 | .editorconfig 4 | .npmignore 5 | demo/ 6 | test/ 7 | index.html 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./specs', true, /\.js$/) 2 | context.keys().forEach(context) 3 | 4 | var srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 5 | srcContext.keys().forEach(srcContext) 6 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('../webpack.config') 2 | webpackConfig.entry = {} 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | browsers: ['PhantomJS'], 7 | frameworks: ['mocha', 'sinon-chai'], 8 | reporters: ['spec'], 9 | files: ['index.js'], 10 | preprocessors: { 11 | 'index.js': ['webpack'] 12 | }, 13 | webpack: webpackConfig, 14 | webpackMiddleware: { 15 | noInfo: true 16 | }, 17 | singleRun: true 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/translator.js: -------------------------------------------------------------------------------- 1 | var search = (locale, path) => { 2 | return locale && path.length 3 | ? search(locale[path.shift()], path) 4 | : locale 5 | } 6 | 7 | var replace = (entry, repls) => { 8 | if (!repls) return entry 9 | 10 | return entry.replace(/{(\w+)}/g, (match, index) => { 11 | return repls[index] !== undefined 12 | ? repls[index] 13 | : match 14 | }) 15 | } 16 | 17 | export function translate (locales, lang, path, repls) { 18 | if (!locales) return 19 | 20 | var entry = search(locales[lang], path.split('.')) 21 | 22 | if (typeof entry === 'string') { 23 | return replace(entry, repls) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dist/translator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.translate = translate; 7 | var search = function search(locale, path) { 8 | return locale && path.length ? search(locale[path.shift()], path) : locale; 9 | }; 10 | 11 | var replace = function replace(entry, repls) { 12 | if (!repls) return entry; 13 | 14 | return entry.replace(/{(\w+)}/g, function (match, index) { 15 | return repls[index] !== undefined ? repls[index] : match; 16 | }); 17 | }; 18 | 19 | function translate(locales, lang, path, repls) { 20 | if (!locales) return; 21 | 22 | var entry = search(locales[lang], path.split('.')); 23 | 24 | if (typeof entry === 'string') { 25 | return replace(entry, repls); 26 | } 27 | } -------------------------------------------------------------------------------- /demo/child.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './demo/main.js', 5 | output: { 6 | path: './demo/build', 7 | publicPath: '/build/', 8 | filename: 'build.js' 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.vue$/, 14 | loader: 'vue' 15 | }, 16 | { 17 | test: /\.js$/, 18 | loader: 'babel', 19 | exclude: /node_modules/ 20 | } 21 | ] 22 | }, 23 | devServer: { 24 | historyApiFallback: true, 25 | noInfo: true 26 | }, 27 | devtool: '#eval-source-map' 28 | } 29 | 30 | if (process.env.NODE_ENV === 'production') { 31 | module.exports.devtool = '#source-map' 32 | module.exports.plugins = (module.exports.plugins || []).concat([ 33 | new webpack.DefinePlugin({ 34 | 'process.env': { 35 | NODE_ENV: '"production"' 36 | } 37 | }), 38 | new webpack.optimize.UglifyJsPlugin({ 39 | compress: { 40 | warnings: false 41 | } 42 | }), 43 | new webpack.optimize.OccurenceOrderPlugin() 44 | ]) 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pantelis Peslis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | } 4 | 5 | pre { 6 | padding: 24px; 7 | font-family: 'Roboto Slab'; 8 | font-size: 10px; 9 | line-height: 2; 10 | letter-spacing: 2px; 11 | border-radius: 4px; 12 | opacity: 0.5; 13 | } 14 | 15 | iframe { 16 | margin: 0 4px; 17 | } 18 | 19 | .content ul { 20 | margin-right: 0; 21 | } 22 | 23 | .hero-top { 24 | padding: 40px 0; 25 | background: linear-gradient(to bottom, transparent 0%, #fff 100%) center bottom no-repeat, 26 | url(bg.jpg) center 10% no-repeat; 27 | background-size: 100% 100%, cover; 28 | } 29 | 30 | .hero-top .notification { 31 | padding: 0; 32 | background-color: transparent; 33 | font-family: Courgette; 34 | font-size: 23px; 35 | line-height: 1.7; 36 | letter-spacing: 2px; 37 | } 38 | 39 | .hero.hero-title .title { 40 | font-family: Courgette; 41 | font-size: 40px; 42 | color: #ED6C63; 43 | } 44 | 45 | .hero.hero-title .hero-content { 46 | padding-bottom: 0; 47 | } 48 | 49 | .global { 50 | background: #42afe3; 51 | } 52 | 53 | .footer { 54 | padding: 40px 0; 55 | background: transparent; 56 | } 57 | 58 | .footer iframe { 59 | transition: opacity 0.25s; 60 | opacity: 0.5; 61 | } 62 | 63 | .footer iframe:hover { 64 | opacity: 1; 65 | } 66 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueLocalizer from '../src/index' 3 | import NProgress from 'nprogress' 4 | import Child from './child.vue' 5 | 6 | // install 7 | Vue.use(VueLocalizer) 8 | 9 | // global locales 10 | var locales = { 11 | en: { 12 | name: { 13 | first: 'Pantelis', 14 | last: 'Peslis' 15 | }, 16 | color: 'Blue' 17 | }, 18 | el: { 19 | name: { 20 | first: 'Παντελής', 21 | last: 'Πεσλής' 22 | }, 23 | color: 'Μπλε' 24 | } 25 | } 26 | 27 | // create an instance 28 | var localizer = new VueLocalizer(locales) 29 | 30 | // add hooks 31 | localizer.beforeChange(NProgress.start) 32 | localizer.afterChange(NProgress.done) 33 | 34 | new Vue({ 35 | el: 'body', 36 | data: { 37 | selected: 'en', 38 | globalLocales: locales 39 | }, 40 | components: { 41 | Child 42 | }, 43 | locales: { 44 | en: { 45 | color: 'Yellow', 46 | number: { 47 | list: 'Numbers: {0} 2 {1} 4' 48 | } 49 | }, 50 | el: { 51 | color: 'Κίτρινο', 52 | number: { 53 | list: 'Αριθμοί: {0} 2 {1} 4' 54 | } 55 | } 56 | }, 57 | methods: { 58 | change (lang) { 59 | this.$lang.change(lang) 60 | this.selected = lang 61 | }, 62 | isSelected (lang) { 63 | return { 64 | 'is-info': this.$lang.get() === lang 65 | } 66 | } 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { translate } from './translator' 2 | 3 | var Vue 4 | 5 | var data = { 6 | _lang: { value: '' }, 7 | 8 | locales: {}, 9 | 10 | get lang () { 11 | return this._lang.value 12 | }, 13 | 14 | set lang (value) { 15 | this._lang.value = value 16 | } 17 | } 18 | 19 | class Localizer { 20 | constructor (locales = {}, lang = 'en') { 21 | if (!Localizer.installed) { 22 | throw new Error( 23 | 'Please install the Localizer with Vue.use() before creating an instance.' 24 | ) 25 | } 26 | 27 | data.lang = lang 28 | data.locales = locales 29 | 30 | Vue.util.defineReactive({}, null, data._lang) 31 | 32 | Vue.prototype.$lang = function (path, repls) { 33 | // search for the path 'locally' 34 | return translate(this.$options.locales, data.lang, path, repls) 35 | // search for the path 'globally' 36 | || translate(data.locales, data.lang, path, repls) 37 | // if the path does not exist, return the path 38 | || path 39 | } 40 | 41 | Object.assign(Vue.prototype.$lang, { 42 | change: this.change.bind(this), 43 | get: () => data.lang 44 | }) 45 | } 46 | 47 | change (lang) { 48 | if (data.lang === lang) return 49 | 50 | this._before && this._before(data.lang) 51 | 52 | data.lang = lang 53 | 54 | this._after && this._after(data.lang) 55 | } 56 | 57 | beforeChange (fn) { 58 | this._before = fn 59 | } 60 | 61 | afterChange (fn) { 62 | this._after = fn 63 | } 64 | } 65 | 66 | Localizer.installed = false 67 | 68 | Localizer.install = (vue) => { 69 | Vue = vue 70 | Localizer.installed = true 71 | } 72 | 73 | export default Localizer 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Localizer 2 | 3 | ## Installation 4 | 5 | ### Install via NPM 6 | 7 | Available through npm as `vue-localizer`. 8 | ``` 9 | npm install --save vue-localizer 10 | ``` 11 | 12 | ## Usage 13 | You can set the locales globally or.. into each component separately! 14 | 15 | - Firsty it checks about the given path into the component's locales. 16 | - If it exist, returns the specified text. 17 | - Otherwise it checks into the global locales and if it does not exist, it returns the path. 18 | 19 | #### Create a new Localizer instance 20 | ```js 21 | import VueLocalizer from 'vue-localizer' 22 | 23 | // install 24 | Vue.use(VueLocalizer) 25 | 26 | // the constructor comes with 2 optional arguments: 27 | // - globalLocales (default: {}) 28 | // - language (default: 'en') 29 | var localizer = new VueLocalizer() 30 | ``` 31 | 32 | #### You can add before/after change hooks 33 | ```js 34 | localizer.beforeChange((lang) => {}) 35 | localizer.afterChange((lang) => {}) 36 | ``` 37 | 38 | #### Add locales to your components if you want 39 | ```js 40 | export default { 41 | data: {..}, 42 | 43 | locales: { 44 | en: { 45 | name: { 46 | first: 'Pantelis', 47 | last: '{0}' 48 | full: '{first} {last}', 49 | } 50 | }, 51 | el: {..}, 52 | .. 53 | }, 54 | 55 | methods: {..} 56 | } 57 | ``` 58 | 59 | #### Change the language 60 | ```js 61 | this.$lang.change('el') // call it from a vue instance 62 | ``` 63 | 64 | #### Template 65 | ```html 66 | 67 | {{ $lang('name.first') }} 68 | 69 | 70 | {{ $lang('name.last', ['Peslis']) }} 71 | 72 | 73 | {{ $lang('name.full', {first:'Pantelis',last:'Peslis'}) }} 74 | ``` 75 | 76 | ## License 77 | Localizer is released under the MIT License. See the bundled LICENSE file for details. 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-localizer", 3 | "version": "1.1.1", 4 | "author": "Pantelis Peslis ", 5 | "license": "MIT", 6 | "description": "Localization plugin for Vue.js", 7 | "keywords": [ 8 | "vue", 9 | "localizer", 10 | "locale", 11 | "localization", 12 | "internationalization", 13 | "i18n" 14 | ], 15 | "main": "dist/index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/pespantelis/vue-localizer.git" 19 | }, 20 | "bugs": "https://github.com/pespantelis/vue-localizer/issues", 21 | "homepage": "http://pespantelis.github.io/vue-localizer/", 22 | "scripts": { 23 | "dev": "webpack-dev-server --content-base demo/ --inline --hot", 24 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", 25 | "compile": "babel -d dist/ src/", 26 | "test": "karma start test/karma.conf.js" 27 | }, 28 | "dependencies": { 29 | "babel-runtime": "^5.8.0", 30 | "vue": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^6.0.0", 34 | "babel-loader": "^6.0.0", 35 | "babel-plugin-transform-runtime": "^6.0.0", 36 | "babel-preset-es2015": "^6.0.0", 37 | "babel-preset-stage-2": "^6.0.0", 38 | "chai": "^3.5.0", 39 | "cross-env": "^1.0.6", 40 | "css-loader": "^0.23.0", 41 | "karma": "^0.13.22", 42 | "karma-mocha": "^0.2.2", 43 | "karma-phantomjs-launcher": "^1.0.0", 44 | "karma-sinon-chai": "^1.2.0", 45 | "karma-spec-reporter": "0.0.26", 46 | "karma-webpack": "^1.7.0", 47 | "lolex": "^1.4.0", 48 | "mocha": "^2.4.5", 49 | "phantomjs-prebuilt": "^2.1.7", 50 | "sinon": "^1.17.3", 51 | "sinon-chai": "^2.8.0", 52 | "vue-hot-reload-api": "^1.2.0", 53 | "vue-html-loader": "^1.0.0", 54 | "vue-loader": "^8.2.1", 55 | "vue-style-loader": "^1.0.0", 56 | "webpack": "^1.12.2", 57 | "webpack-dev-server": "^1.12.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/specs/translator.spec.js: -------------------------------------------------------------------------------- 1 | import { translate } from '../../src/translator' 2 | 3 | var locales = { 4 | langA: { 5 | a: 'foo', 6 | b: { c: 'bar' } 7 | }, 8 | langB: { 9 | a: 'baz' 10 | }, 11 | langC: { 12 | a: 'foo {0} baz {1}', 13 | b: 'foo {a} baz {b}' 14 | } 15 | } 16 | 17 | describe('search', () => { 18 | it('returns the existed entry', () => { 19 | translate(locales, 'langA', 'a').should.equal('foo') 20 | translate(locales, 'langA', 'b.c').should.equal('bar') 21 | translate(locales, 'langB', 'a').should.equal('baz') 22 | }) 23 | 24 | it('returns undefined, if the locales is empty', () => { 25 | expect(translate({}, 'langA', 'a')).to.not.exist 26 | expect(translate(null, 'langA', 'a')).to.not.exist 27 | }) 28 | 29 | it('returns undefined, if the path does not exist', () => { 30 | expect(translate(locales, 'langA', 'b.c.d')).to.not.exist 31 | }) 32 | 33 | it('returns undefined, if the path is an object', () => { 34 | expect(translate(locales, 'langA', 'b')).to.not.exist 35 | }) 36 | }) 37 | 38 | describe('replace', () => { 39 | it('returns the entry, if there is not replacements', () => { 40 | translate(locales, 'langA', 'a').should.equal('foo') 41 | }) 42 | 43 | it('replaces with list params', () => { 44 | translate(locales, 'langC', 'a', [0,1]).should.equal('foo 0 baz 1') 45 | translate(locales, 'langC', 'a', ['bar','qux']).should.equal('foo bar baz qux') 46 | translate(locales, 'langC', 'a', ['bar']).should.equal('foo bar baz {1}') 47 | }) 48 | 49 | it('replaces with named params', () => { 50 | translate(locales, 'langC', 'b', {a:0,b:1}).should.equal('foo 0 baz 1') 51 | translate(locales, 'langC', 'b', {a:'bar',b:'qux'}).should.equal('foo bar baz qux') 52 | translate(locales, 'langC', 'b', {b:'bar',a:'qux'}).should.equal('foo qux baz bar') 53 | translate(locales, 'langC', 'b', {c:'bar',b:'qux'}).should.equal('foo {a} baz qux') 54 | translate(locales, 'langC', 'b', {b:'bar'}).should.equal('foo {a} baz bar') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _assign = require('babel-runtime/core-js/object/assign'); 8 | 9 | var _assign2 = _interopRequireDefault(_assign); 10 | 11 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 12 | 13 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 14 | 15 | var _createClass2 = require('babel-runtime/helpers/createClass'); 16 | 17 | var _createClass3 = _interopRequireDefault(_createClass2); 18 | 19 | var _translator = require('./translator'); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | var Vue; 24 | 25 | var data = { 26 | _lang: { value: '' }, 27 | 28 | locales: {}, 29 | 30 | get lang() { 31 | return this._lang.value; 32 | }, 33 | 34 | set lang(value) { 35 | this._lang.value = value; 36 | } 37 | }; 38 | 39 | var Localizer = function () { 40 | function Localizer() { 41 | var locales = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 42 | var lang = arguments.length <= 1 || arguments[1] === undefined ? 'en' : arguments[1]; 43 | (0, _classCallCheck3.default)(this, Localizer); 44 | 45 | if (!Localizer.installed) { 46 | throw new Error('Please install the Localizer with Vue.use() before creating an instance.'); 47 | } 48 | 49 | data.lang = lang; 50 | data.locales = locales; 51 | 52 | Vue.util.defineReactive({}, null, data._lang); 53 | 54 | Vue.prototype.$lang = function (path, repls) { 55 | // search for the path 'locally' 56 | return (0, _translator.translate)(this.$options.locales, data.lang, path, repls) 57 | // search for the path 'globally' 58 | || (0, _translator.translate)(data.locales, data.lang, path, repls) 59 | // if the path does not exist, return the path 60 | || path; 61 | }; 62 | 63 | (0, _assign2.default)(Vue.prototype.$lang, { 64 | change: this.change, 65 | get: function get() { 66 | return data.lang; 67 | } 68 | }); 69 | } 70 | 71 | (0, _createClass3.default)(Localizer, [{ 72 | key: 'change', 73 | value: function change(lang) { 74 | if (data.lang === lang) return; 75 | 76 | this._before && this._before(data.lang); 77 | 78 | data.lang = lang; 79 | 80 | this._after && this._after(data.lang); 81 | } 82 | }, { 83 | key: 'beforeChange', 84 | value: function beforeChange(fn) { 85 | this._before = fn; 86 | } 87 | }, { 88 | key: 'afterChange', 89 | value: function afterChange(fn) { 90 | this._after = fn; 91 | } 92 | }]); 93 | return Localizer; 94 | }(); 95 | 96 | Localizer.installed = false; 97 | 98 | Localizer.install = function (vue) { 99 | Vue = vue; 100 | Localizer.installed = true; 101 | }; 102 | 103 | exports.default = Localizer; -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Localizer | Vue.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |

19 | Any component responds to its own template, style and script.. 20 |

21 | But it also requires its own locales! 22 |

23 |
24 |
25 |

26 | Localizer 27 |

28 |

29 | Localization plugin for Vue.js 30 |

31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |

42 | Give it a try 43 |

44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |

Main Vue Instance

56 |
    57 |
  • 58 | $lang('color') 59 |

    ↪ {{ $lang('color') }}

    60 |
  • 61 |
  • 62 | $lang('name.last') 63 |

    ↪ {{ $lang('name.last') }}

    64 |
  • 65 |
  • 66 | $lang('number.list', ['1','3']) 67 |

    ↪ {{ $lang('number.list', ['1','3']) }}

    68 |
  • 69 |
  • 70 | $lang('path.does.not.exist') 71 |

    ↪ {{ $lang('path.does.not.exist') }}

    72 |
  • 73 |
74 |
{{ $options.locales | json }}
75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 |

Global Locales

85 |
{{ globalLocales | json }}
86 |
87 |
88 |
89 |
90 |
91 |
92 | 100 | 101 | 102 | 103 | 104 | --------------------------------------------------------------------------------