├── .gitignore ├── .npmignore ├── README.md ├── build ├── css-loaders.js ├── dev-server.js └── webpack.config.js ├── package.json ├── src ├── libs │ ├── translate.js │ └── utils.js ├── vue-localize-directive.js ├── vue-localize.js └── vuex-getters.js └── test └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── utils ├── _data ├── vue-localize-conf.js └── vue-localize-translations.js ├── has.spec.js ├── recursively.spec.js ├── replace.spec.js └── translate.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | /node_modules 4 | /dist/* 5 | test/unit/coverage/* 6 | !.gitignore 7 | vue-localize.sublime-project 8 | vue-localize.sublime-workspace 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | src/ 4 | node_modules/ 5 | .gitignore 6 | .babelrc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-localize 2 | 3 | > Localization plugin for implementation multilingual functionality in VueJS based applications with [Vuex](https://github.com/vuejs/vuex) and [VueRouter](https://github.com/vuejs/vue-router) 4 | 5 | ## Demo 6 | 7 | [Working example](http://env-5533755.j.layershift.co.uk/vue-localize-example/dist/) 8 | 9 | [Example sources](https://github.com/Saymon-biz/vue-localize-example) 10 | 11 | ## Important 12 | You can NOT use this plugin without Vuex 13 | 14 | You can NOT use this plugin without VueRouter, however it will be possible at an early date (and this line will be deleted immediately). 15 | 16 | ## Links 17 | 18 | - [VueRouter](http://vuejs.github.io/vue-router/en/index.html) 19 | - [Vuex](http://vuex.vuejs.org/en/index.html) 20 | 21 | ## Functionality and features 22 | - Easy integration with your application 23 | - Current language is a Vuex state changed only via mutations 24 | - Saving selected language in the browser's local storage 25 | - Fallback language support 26 | - Automatic routes localization (adding leading language part to the routes paths): ```/about ===> /en/about, /ru/about,...``` (only with official VueRouter) 27 | - Wrapper for route name for using in v-link for proper navigation: ``` v-link="{name: $localizeRoute('about')}" ``` 28 | - `````` tag translation 29 | - Route path translation: ``` $localizeRoutePath($route, lang) ``` 30 | - Option for excluding language part from route path for default language 31 | - Option for custom name of the key in local storage 32 | - Global mixin for getting current language in Vue components via Vuex getter "currentLanguage" 33 | - Translating phrases via Vue filter: ```{{ phrase | translate }}``` 34 | - Translating phrases via direct call of plugin method: ``` {{ $translate(phrase) }} or v-text="$translate(phrase)" ``` 35 | - Translating phrases via Vue directive: ``` v-localize="{path: 'header.nav.home'}" ``` 36 | - Injection custom variables into translations: ``` {{ $translate(phrase, objVars) }} ``` 37 | - Translating into exact language regardless of current selected: ``` {{ $translate(phrase, null, 'en') }} ``` 38 | - Reactive UI translating via language selector 39 | - Flexible context-based translations structure 40 | - Language selector inplementation tutorial 41 | - Separate NPM package 42 | 43 | ## Installation 44 | 45 | In your project folder (where is package.json) 46 | 47 | ```bash 48 | $ npm install vue-localize --save 49 | ``` 50 | 51 | ## Integration 52 | > Full-featured example of integration in app with Vuex and VueRouter. 53 | 54 | To use full set of VueLocalize features you need to do following simple steps: 55 | - Integrate plugin 56 | - Import and register plugin 57 | - Add Vuex store module 58 | - Setup initial state 59 | - Create and adjust the configuration file 60 | - Create file with translations 61 | - Add option ```localized: true``` into root-level routes, which need to become internationalized 62 | 63 | #### Importing and registering VueLocalize plugin 64 | > In your entry point (usually it's index.js or main.js) 65 | 66 | ```js 67 | import Vue from 'vue' 68 | 69 | import VueRouter from 'vue-router' 70 | Vue.use(VueRouter) 71 | 72 | var router = new VueRouter({ 73 | // your set of options 74 | }) 75 | 76 | // Import router config obejct 77 | import routes from './config/routes' 78 | 79 | // Import plugin config 80 | import vlConfig from './config/vue-localize-conf' 81 | 82 | // Import vuex store (required by vue-localize) 83 | import store from './vuex/store' 84 | 85 | // Import VueLocalize plugin 86 | import VueLocalize from 'vue-localize' 87 | 88 | Vue.use(VueLocalize, {// All options is required 89 | store, 90 | config: vlConfig, 91 | router: router, 92 | routes: routes 93 | }) 94 | 95 | // Import App component - root Vue instance of application 96 | import App from './App' 97 | 98 | // Application start 99 | router.start(App, '#app') 100 | ``` 101 | Pay attention (!) there is no line with ```router.map(routes)``` in the code above. 102 | When using automatic routes localization, VueLocalize will transform your initial router config and VueRouter will use it already transformed. So this line of code is built into the plugin. The more detailed explanation provided below. 103 | 104 | #### Adding Vuex store module 105 | 106 | > Note that VueLocalize contains built-in Vuex store module, so if Vuex states and mutations in your application don't splitted in sub-modules, it's time to do so. Here you can find the information about how to split state and mutations in sub modules [http://vuex.vuejs.org/en/structure.html](http://vuex.vuejs.org/en/structure.html) 107 | 108 | > Also note that it is important to use the exact name of module ```vueLocalizeVuexStoreModule``` in your code. 109 | 110 | Code for your store.js 111 | ```js 112 | import Vuex from 'vuex' 113 | import Vue from 'vue' 114 | import { vueLocalizeVuexStoreModule } from 'vue-localize' 115 | // import other Vuex modules 116 | 117 | Vue.use(Vuex) 118 | 119 | export default new Vuex.Store({ 120 | modules: { 121 | vueLocalizeVuexStoreModule, 122 | // other Vuex modules 123 | } 124 | }) 125 | ``` 126 | 127 | #### Setting up an initial state 128 | You can't know in advance, what exact route will initialize your application. It can be either route with leading language part or without. And VueLocalize needs to understand, what exact language it should set as the initial. It can be a language from the route, or saved in local storage if there is no language part in route (e.g. in the admin section), or the default language. 129 | 130 | And there is the global method ```$vueLocalizeInit($route)``` for this purpose. It's just a function which gets a route object as the attribute. 131 | 132 | We recommend place the call of this method in the ready hook of the root Vue instance, which was passed in ```router.start()``` 133 | In our example it's the App.vue component. 134 | ```js 135 | <script> 136 | import store from '../vuex/store' 137 | export default { 138 | // injecting Vuex store into the root Vue instance 139 | store, 140 | ready: function () { 141 | this.$vueLocalizeInit(this.$route) 142 | } 143 | } 144 | </script> 145 | ``` 146 | 147 | ## Configuration file 148 | 149 | ```js 150 | import translations from './vue-localize-translations' 151 | export default { 152 | languages: { 153 | en: { 154 | key: 'eng', 155 | enabled: true 156 | }, 157 | ru: { 158 | key: 'rus', 159 | enabled: true 160 | }, 161 | de: { 162 | key: 'deu', 163 | enabled: false 164 | } 165 | }, 166 | defaultLanguage: 'en', 167 | translations: translations, 168 | defaultLanguageRoute: true, 169 | resaveOnLocalizedRoutes: false, 170 | defaultContextName: 'global', 171 | fallbackOnNoTranslation: false, 172 | fallbackLanguage: 'en', 173 | supressWarnings: false 174 | } 175 | ``` 176 | #### Options explanation 177 | - **languages** - The list of application languages. It's just a JSON object. The key of each root node is a language code. Each node is a configuration of the exact language and should contain two options: 178 | - **key** - phrase key in the translation file for translating language name into different languages, e.g. for rendering items in language selector 179 | - **enabled** - set to false if need to disable language, or true to enable 180 | 181 | 182 | - **defaultLanguage** - Default language will be used if the language not defined in current route, Vuex store or localStorage. Usually, it used when user came for the first time 183 | 184 | - **translations** - The object with translations of application phrases 185 | 186 | - **defaultLanguageRoute** - Defines the behavior of routes localization (adding language at the start of path of the routes). If false, then disable leading language param for all routes with default language, otherwise enable 187 | 188 | - **resaveOnLocalizedRoutes** - Defines the policy for storing selected language in browser local storage for transitions from localized routes to not localized and backward. If false, the transition from NOT localized route to localized will not update selected language in local storage, and it will be taken up when you'll back TO NOT localized route FROM LOCALIZED, even you have switched languages with language selector. It can be useful in case when you need to remember the language selected in user account or administrative panel and switching languages at the public section of a site should not affect this choice. Set it to true if you need transparent behavior of the application when switching languages and the language needs to be changed for all application regardless of where exactly it was switched, in administration panel or at the public section of a site. 189 | 190 | - **defaultContextName** - Name of the key for default context of translations 191 | 192 | - **fallbackOnNoTranslation** - Set to true if you want to translate phrases which have no translation in the language defined in the option "fallbackLanguage" below. It may be useful when you already need to publish your app, but you have no complete translations for all languages and for all phrases 193 | 194 | - **fallbackLanguage** - Defines the fallback language for using in case described in comment for the option above 195 | 196 | - **supressWarnings** - Suppress warnings emitted into browser console (concerns only translation process). Plugin can emit warnings during translation phrases process in the following cases: 197 | - phrase path doesn't exist in localization file (emitted always) 198 | - phrase path exists but there is no translation for the current language (emitted only if "fallbackOnNoTranslation" is set to false) 199 | - phrase path exists, but it hasn't a translation for the current language and hasn't translation for the fallback language (emitted only if "fallbackOnNoTranslation" is set to true) 200 | - output translation contains unprocessed variables which will be shown to the user as is, e.g. %foo% 201 | 202 | ## Translations file structure, contexts and global context 203 | 204 | Translations structure is just a JSON object, so you can to structure translations as you want. 205 | 206 | ```js 207 | export default { 208 | // global context 209 | 'global': { 210 | 'project-name': { 211 | en: 'Name of the project in English', 212 | ru: 'Название проекта на Русском' 213 | }, 214 | // translations for language selector items 215 | lang: { 216 | eng: { 217 | en: 'English', 218 | ru: 'Английский' 219 | }, 220 | rus: { 221 | en: 'Russian', 222 | ru: 'Русский' 223 | } 224 | } 225 | }, 226 | // context for translations of frontend phrases (public section of your site) 227 | 'site': { 228 | // context for translations of header 229 | 'header': { 230 | // context for translations for anchors in nav menu 231 | 'nav': { 232 | // key of the anchor of the link to homepage 233 | 'home': { 234 | // translation of the anchor of the link to homepage into the English 235 | en: 'Home', 236 | ru: 'Главная' 237 | }, 238 | // key of the anchor of the link to about page 239 | 'about': { 240 | en: 'About', 241 | ru: 'О проекте' 242 | }, 243 | // key of the anchor of the link to contacts page 244 | 'contacts': { 245 | en: 'Contacts', 246 | ru: 'Контакты' 247 | }, 248 | 'loginbox': { 249 | // ... 250 | } 251 | } 252 | }, 253 | 'footer': { 254 | // translations of footer phrases 255 | } 256 | }, 257 | 'admin': { 258 | // translations of administration panel phrases 259 | } 260 | } 261 | ``` 262 | E.g. to get the translation of the anchor of the link to homepage into the current language, you should pass the path to the phrase key to the translation mechanism. In this case site.header.nav.home is the path, the part site.header.nav of this path is the "context" and home is the key of a phrase. So the path to any node which does not contain leafs is a context, each node which contains leafs is a key of a phrase and each leaf is the translation for the exact language. 263 | 264 | #### Global context 265 | The Global context is the root-level key, defined in the corresponding option of the VueLocalize configuration file. The feature of the global context is that you don't need include its name in the path which passing into translation method/filter/directive. E.g. to translate phrase with path ```global.project-name``` you can write just ```{{ 'project-name' | translate }}``` instead of full path ```global.project-name```. 266 | 267 | ## Router config for automatic routes localization 268 | > Example below assumes an application of a website, that consists of the public and administrative sections and assumes that the public section should working with localized routes paths and the administrative section shouldn't. 269 | 270 | ```js 271 | import SiteLayout from './components/SiteLayout' 272 | import HomePage from './components/HomePage' 273 | import SiteLayout from './components/AboutPage' 274 | import SiteLayout from './components/ContactsPage' 275 | import SiteLayout from './components/AdminLayout' 276 | // ... importing other components 277 | 278 | export default { 279 | // the parent route for public section of your application 280 | '/': { 281 | localized: true, // (!!!) the only thing you have to add for localize this route and all nested routes recursively 282 | component: SiteLayout, 283 | subRoutes: { 284 | '/': { 285 | name: 'home-page', 286 | component: HomePage 287 | }, 288 | '/about': { 289 | name: 'about-page', 290 | component: AboutPage 291 | }, 292 | '/contacts': { 293 | name: 'contacts-page', 294 | component: ContactsPage 295 | }, 296 | } 297 | }, 298 | '/admin': { 299 | component: AdminLayout 300 | subRoutes: { 301 | // administration area subroutes 302 | } 303 | } 304 | }) 305 | 306 | ``` 307 | Pay attention to the ```localized: true``` option of the parent route for public section of application. This is really the only thing you have to add to internationalize path of this and all nested routes recursively. And you have to add this option only in the parent (root-level) routes and not in any sub routes. 308 | 309 | What will happen? 310 | 311 | If use the above described router config as is, we'll have the following paths of public section: 312 | ``` 313 | yourdomain.com/ 314 | yourdomain.com/about 315 | yourdomain.com/contacts 316 | ``` 317 | 318 | VueLocalize will transform initial config automatically and in the end we'll have the following set of paths: 319 | 320 | ``` 321 | yourdomain.com/en 322 | yourdomain.com/en/about 323 | yourdomain.com/en/contacts 324 | yourdomain.com/ru 325 | yourdomain.com/ru/about 326 | yourdomain.com/ru/contacts 327 | ``` 328 | 329 | Transitions between routes e.g. ```yourdomain.com/en/about``` and ```yourdomain.com/ru/about``` (when switching languages via language selector) will reuse the same component. So if you have any data on the page (in the component bound to the current route), and the switching to another language, data will not be affected despite the fact that the route has been actually changed. VueLocalize simply performs reactive translation of all the phrases at the page. 330 | 331 | ##### Excluding leading language part from routes paths for default language 332 | Note that it's easy to exclude leading language part from routes for default language if needed. 333 | E.g. if English is defined as default application language, the only thing we have to do for - set to ```false``` the ```defaultLanguageRoute``` option in the config. Then we'll have the following set of paths: 334 | 335 | ``` 336 | # for English 337 | yourdomain.com/ 338 | yourdomain.com/about 339 | yourdomain.com/contacts 340 | # for Russian 341 | yourdomain.com/ru 342 | yourdomain.com/ru/about 343 | yourdomain.com/ru/contacts 344 | ``` 345 | And the dump of the transformed router config below helps to understand better what will happen with initial router config and how exactly it will be transformed. 346 | ```js 347 | export default { 348 | '/en': { 349 | localized: true, 350 | lang: 'en', 351 | component: SiteLayout, 352 | subRoutes: { 353 | '/': { 354 | name: 'en_home-page', 355 | component: HomePage 356 | }, 357 | '/about': { 358 | name: 'en_about-page', 359 | component: AboutPage 360 | }, 361 | '/contacts': { 362 | name: 'en_contacts-page', 363 | component: ContactsPage 364 | }, 365 | } 366 | }, 367 | '/ru': { 368 | localized: true, 369 | lang: 'ru', 370 | component: SiteLayout, 371 | subRoutes: { 372 | '/': { 373 | name: 'ru_home-page', 374 | component: HomePage 375 | }, 376 | '/about': { 377 | name: 'ru_about-page', 378 | component: AboutPage 379 | }, 380 | '/contacts': { 381 | name: 'ru_contacts-page', 382 | component: ContactsPage 383 | }, 384 | } 385 | }, 386 | '/admin': { 387 | component: AdminLayout 388 | subRoutes: { 389 | // ... 390 | } 391 | } 392 | }) 393 | ``` 394 | As you can see 395 | - root-level routes (only which have ```localized: true ``` option) will be cloned from initial one recursively 396 | - leading parts with language codes will be added into the paths of root-level routes 397 | - names for all sub routes will be changed recursively by adding prefixes with language code 398 | - option ```lang``` with language code in value will be added into root-level routes only 399 | 400 | There is two important things you should consider when using this plugin: 401 | - option ```lang``` added into the root-level routes. Just keep it in mind. 402 | - changing names of the routes. And there is the special global method of the VueLocalize plugin for wrapping initial route name in ```v-link``` directive. To implement navigation for multilingual routes with VueLocalize, just do the following: 403 | ```html 404 | <a v-link="{name: $localizeRoute('about')}"></a> 405 | ``` 406 | Method ```$localizeRoute()``` works only with names of routes, but not with paths, so routes used in navigation links should be named. And, please, don't use unnamed routes / sub-routes to avoid unexpected behavior. This case (using unnamed routes with this plugin) is not tested yet. 407 | 408 | ## Language selector example 409 | Simple selector with bootstrap dropdown 410 | ```html 411 | <template> 412 | <li class="dropdown" :class="{'open': opened}"> 413 | <a href="javascript:;" @click="toggle">{{ currentLanguage | uppercase }} <span class="caret"></span></a> 414 | <ul class="dropdown-menu"> 415 | <li v-for="(code, language) in $localizeConf.languages" v-if="code !== currentLanguage && language.enabled !== false"> 416 | <a href="{{ $localizeRoutePath($route, code) }}" @click.prevent="changeLanguage(code)"> 417 | {{ code | uppercase }} | {{ 'global.lang.' + language.key | translate null code }}<br /> 418 | <small class="text-muted">{{ 'global.lang.' + language.key | translate null currentLanguage }}</small> 419 | </a> 420 | </li> 421 | </ul> 422 | </li> 423 | </template> 424 | <script> 425 | import { replace } from 'lodash' 426 | export default { 427 | data () { 428 | return { 429 | opened: false 430 | } 431 | }, 432 | methods: { 433 | toggle: function () { 434 | this.opened = !this.opened 435 | }, 436 | changeLanguage: function (code) { 437 | this.toggle() 438 | if (!this.$route.localized) { 439 | this.$store.dispatch('SET_APP_LANGUAGE', code) 440 | } else { 441 | var oldRouteName = this.$route.name 442 | var routeName = replace(oldRouteName, /^[a-z]{2}/g, code) 443 | this.$router.go({name: routeName}) 444 | } 445 | } 446 | } 447 | } 448 | </script> 449 | ``` 450 | The example above uses the following features: 451 | - ```$localizeConf``` - global property of the VueLocalize plugin, which contains the configuration object from the VueLocalize config file 452 | - ```currentLanguage``` - global mixin which is just the proxy to Vuex getter for accessing reactive state of current language in Vuex store 453 | - ```$localizeRoutePath()``` - global method of the VueLocalize plugin for translating path of the route to another language 454 | - ```this.$store.dispatch('SET_APP_LANGUAGE', code)``` - dispatch the mutation 455 | 456 | Read more about these features in the "API" section below. 457 | Pay attention that in the example above we dispatch mutation only for non localized routes, but if route has flag ```localized: true``` we perform ```router.go()``` and in this case mutation will be dispatched automatically inside the VueLocalize plugin 458 | 459 | ## Usage 460 | 461 | #### Translating 462 | 463 | VueLocalize provides three ways for translating phrases: 464 | - via **Vue filter** 465 | - via **direct call** of the plugin method 466 | - via **Vue directive** ```v-localize``` 467 | 468 | Ultimately in all these cases translation will be performed by the same internal mechanism of the plugin, which is just a function with following three arguments: ```(path, [vars], [lang])``` 469 | 470 | - *path* - (required) - the path to the key of a phrase in the JSON object with translations (explained slightly above). 471 | - *vars* - (optional) - variables to inject into the complete translation (explained slightly below) 472 | - *lang* - (optional) - exact language for translation 473 | 474 | Let's look at examples of usage listed above different translating methods 475 | 476 | #### Translating via Vue filter 477 | 478 | Just a translating into the current (selected) language 479 | ```html 480 | <span>{{ 'site.header.nav.home' | translate }}</span> 481 | ``` 482 | 483 | Translating into exact language, e.g. English 484 | ```html 485 | <span>{{ 'site.header.nav.home' | translate null 'en' }}</span> 486 | ``` 487 | 488 | #### Translating via direct method call 489 | 490 | Translating into current language 491 | ```html 492 | <span>{{ $translate('site.header.nav.home') }}</span> 493 | or 494 | <span v-text="{{ $translate('site.header.nav.home') }}"></span> 495 | ``` 496 | 497 | Translating into exact language, e.g. English 498 | ```html 499 | <span>{{ $translate('site.header.nav.home', null, 'en') }}</span> 500 | ``` 501 | 502 | #### Translating via ```v-localize``` directive 503 | 504 | Translating into current language 505 | ```html 506 | <span v-localize="{path: 'site.header.nav.home'}"></span> 507 | ``` 508 | 509 | Translating into exact language, e.g. English 510 | ```html 511 | <span v-localize="{path: 'site.header.nav.home', lang: 'en'}"></span> 512 | ``` 513 | 514 | ## Injection custom variables into complete translation 515 | 516 | Lets define some variables just for example 517 | ```js 518 | export default { 519 | data () { 520 | return { 521 | vars: { 522 | '%foo%': 'Foo', 523 | '%bar%': 'Bar' 524 | } 525 | } 526 | } 527 | } 528 | ``` 529 | and add the example phrase with translations into the global context: 530 | ```js 531 | export default { 532 | // global context 533 | 'global': { 534 | 'project-name': { 535 | en: 'Name of the project in English', 536 | ru: 'Название проекта на Русском' 537 | }, 538 | 'injection-test': { // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< our phrase for injection test 539 | en: 'Some string in English contains %foo% and only then %bar%', 540 | ru: 'Перевод фразы на русский, содержащий наоборот сначала %bar% и только потом %foo%' 541 | }, 542 | // ... 543 | }, 544 | // .... 545 | } 546 | ``` 547 | 548 | #### Injecting with Vue filter 549 | ```html 550 | {{ 'injection-test' | translate vars }} 551 | ``` 552 | #### Injecting with direct call 553 | ```html 554 | {{ $translate('injection-test', vars) }} 555 | ``` 556 | or 557 | ```html 558 | <span v-text="$translate('injection-test', vars)"></span> 559 | ``` 560 | #### Injecting with directive 561 | ```html 562 | <span v-localize="{path: 'injection-test', vars: vars}"></span> 563 | ``` 564 | 565 | # API 566 | 567 | ### Global properties and methods 568 | - **$localizeConf** - global property of the VueLocalize plugin, which contains the configuration object from the VueLocalize config file. So you can access your config in your Vue component just via ```this.$localizeConf``` in models or via ```$localizeConf``` in templates. 569 | 570 | 571 | - **$translate(path, [vars] = null, [lang] = null)** - global function for translating phrases 572 | - **path** - (required) - the path to the key of a phrase in the JSON object with translations 573 | - **vars** - (optional) - variables to inject into the complete translation 574 | - **lang** - (optional) - exact translation language 575 | 576 | 577 | - **$vueLocalizeInit($route)** - method for initialization Vuex state (current language) on page loading/reloading. Detailed explanation describet slightly above in [Setting initial state](#setting-initial-state) 578 | - **$route** - (required) - route object 579 | 580 | 581 | - **$localizeRoute(name, [lang = null])** - method for routes names wrapping for proper navigation. 582 | - **name** - (required) - initial name of a route as defined in your router config 583 | 584 | 585 | - **$localizeRoutePath(route, lang)** - method for translating path of the current route to another language. 586 | - **route** - (required) - route object 587 | - **lang** - (optional) - exact language (using current selected by default) 588 | 589 | 590 | ### Filters 591 | - **translate** 592 | 593 | ### Directives 594 | - **v-localize** 595 | 596 | ### Mixins 597 | - **currentLanguage** - VueLocalize provides the global mixin for getting the current selected language in your Vue components. Mixin is global so will be injected into **each Vue instance** 598 | 599 | ### Mutations 600 | - 'SET_APP_LANGUAGE' - VueLocalize contains built-in Vuex submodule, which provides mutation ```SET_APP_LANGUAGE``` to performing language changing. Only you have to do for change the language from some method of your Vue components - dispatch the mutation. E.g.: 601 | ```js 602 | //... 603 | methods: { 604 | setLanguage: function (language) { 605 | this.$store.dispatch('SET_APP_LANGUAGE', language) 606 | } 607 | }, 608 | //... 609 | 610 | ``` 611 | -------------------------------------------------------------------------------- /build/css-loaders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | module.exports = function (options) { 7 | options = options || {} 8 | // generate loader string to be used with extract text plugin 9 | function generateLoaders (loaders) { 10 | var sourceLoader = loaders.map(function (loader) { 11 | var extraParamChar 12 | if (/\?/.test(loader)) { 13 | loader = loader.replace(/\?/, '-loader?') 14 | extraParamChar = '&' 15 | } else { 16 | loader = loader + '-loader' 17 | extraParamChar = '?' 18 | } 19 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 20 | }).join('!') 21 | 22 | if (options.extract) { 23 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 24 | } else { 25 | return ['vue-style-loader', sourceLoader].join('!') 26 | } 27 | } 28 | 29 | // http://vuejs.github.io/vue-loader/configurations/extract-css.html 30 | return { 31 | css: generateLoaders(['css']), 32 | less: generateLoaders(['css', 'less']), 33 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 34 | scss: generateLoaders(['css', 'sass']), 35 | stylus: generateLoaders(['css', 'stylus']), 36 | styl: generateLoaders(['css', 'stylus']) 37 | } 38 | } -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 25/03/16. 3 | */ 4 | var webpack = require('webpack') 5 | var config = require('./webpack.config') 6 | var compiler = webpack(config) 7 | compiler.watch({ 8 | aggregateTimeout: 300, // wait so long for more changes 9 | poll: true // use polling instead of native watchers 10 | }, function(err, stats) { 11 | if (err) { 12 | console.error(err); 13 | } 14 | console.log('rebuild...') 15 | }); -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require("webpack"); 3 | var version = require("../package.json").version; 4 | var cssLoaders = require('./css-loaders') 5 | var banner = 6 | "/**\n" + 7 | " * vue-localize v" + version + "\n" + 8 | " * https://github.com/Saymon-biz/vue-localize\n" + 9 | " * Released under the MIT License.\n" + 10 | " */\n"; 11 | 12 | module.exports = { 13 | entry: "./src/vue-localize", 14 | output: { 15 | path: "./dist", 16 | filename: "vue-localize.js", 17 | library: "VueLocalize", 18 | libraryTarget: "umd" 19 | }, 20 | plugins: [ 21 | new webpack.BannerPlugin(banner, {raw: true}) 22 | ], 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.js$/, 27 | loader: 'babel', 28 | exclude: /node_modules/, 29 | query: { 30 | presets: ['es2015'], 31 | cacheDirectory: true, 32 | plugins: ["lodash"] 33 | } 34 | }, 35 | { 36 | test: /\.vue$/, 37 | loader: 'vue' 38 | }, 39 | { 40 | test: /\.json$/, 41 | loader: 'json' 42 | }, 43 | { 44 | test: /\.html$/, 45 | loader: 'vue-html' 46 | }, 47 | { 48 | test: /\.(png|jpg|gif|svg|woff2?|eot|ttf)(\?.*)?$/, 49 | loader: 'url', 50 | query: { 51 | limit: 10000, 52 | name: '[name].[ext]?[hash:7]' 53 | } 54 | } 55 | ] 56 | }, 57 | vue: { 58 | loaders: cssLoaders() 59 | }, 60 | resolveLoader: { 61 | fallback: [path.join(__dirname, '../node_modules')] 62 | }, 63 | resolve: { 64 | extensions: ['', '.js', '.vue'], 65 | fallback: [path.join(__dirname, '../node_modules')], 66 | alias: { 67 | 'src': path.resolve(__dirname, '../src') 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-localize", 3 | "version": "1.0.7", 4 | "description": "Localization plugin for Vue.js based applications", 5 | "main": "dist/vue-localize.js", 6 | "scripts": { 7 | "unit": "./node_modules/karma/bin/karma start test/unit/karma.conf.js", 8 | "test": "npm run unit", 9 | "build": "rm -rf dist && mkdirp dist && cross-env NODE_ENV=production webpack -p --progress --hide-modules --config build/webpack.config.js", 10 | "dev": "node build/dev-server.js" 11 | }, 12 | "dependencies": { 13 | "lodash": "^4.6.1", 14 | "vue": "^1.0.17" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.7.4", 18 | "babel-loader": "^6.2.4", 19 | "inject-loader": "^2.0.1", 20 | "isparta-loader": "^2.0.0", 21 | "json-loader": "^0.5.4", 22 | "css-loader": "^0.23.0", 23 | "babel-plugin-lodash": "^2.2.1", 24 | "babel-plugin-transform-runtime": "^6.6.0", 25 | "babel-preset-es2015": "^6.6.0", 26 | "babel-preset-stage-2": "^6.0.0", 27 | "babel-runtime": "^5.8.0", 28 | "cross-env": "^1.0.7", 29 | "cross-spawn": "^2.1.5", 30 | "jasmine-core": "^2.4.1", 31 | "karma": "^0.13.22", 32 | "karma-webpack": "^1.7.0", 33 | "karma-jasmine": "^0.3.8", 34 | "karma-mocha": "^0.2.2", 35 | "karma-phantomjs-launcher": "^1.0.0", 36 | "karma-requirejs": "^0.2.6", 37 | "karma-sourcemap-loader": "^0.3.7", 38 | "karma-spec-reporter": "0.0.24", 39 | "karma-coverage": "^0.5.5", 40 | "mkdirp": "^0.5.1", 41 | "webpack": "^1.12.2", 42 | "webpack-dev-server": "^1.14.1", 43 | "webpack-dev-middleware": "^1.4.0", 44 | "webpack-hot-middleware": "^2.6.0", 45 | "webpack-merge": "^0.8.3", 46 | "extract-text-webpack-plugin": "^1.0.1", 47 | "eslint": "^2.0.0", 48 | "eslint-config-standard": "^5.1.0", 49 | "eslint-friendly-formatter": "^1.2.2", 50 | "eslint-loader": "^1.3.0", 51 | "eslint-plugin-html": "^1.3.0", 52 | "eslint-plugin-promise": "^1.0.8", 53 | "eslint-plugin-standard": "^1.3.2", 54 | "function-bind": "^1.0.2" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/Saymon-biz/vue-localize.git" 59 | }, 60 | "keywords": [ 61 | "Vue", 62 | "i18n", 63 | "localization", 64 | "multilingual", 65 | "vuex" 66 | ], 67 | "author": { 68 | "name": "Saymon", 69 | "email": "saymon.biz@gmail.com" 70 | }, 71 | "license": "MIT", 72 | "bugs": { 73 | "url": "https://github.com/Saymon-biz/vue-localize/issues" 74 | }, 75 | "homepage": "https://github.com/Saymon-biz/vue-localize#readme" 76 | } 77 | -------------------------------------------------------------------------------- /src/libs/translate.js: -------------------------------------------------------------------------------- 1 | import { replace, join, split, each, get } from 'lodash' 2 | import { has } from './utils' 3 | 4 | export class Translator { 5 | /** 6 | * 7 | * @param config 8 | */ 9 | constructor (config, languageGetter) { 10 | this.config = config 11 | this.languageGetter = languageGetter 12 | } 13 | 14 | getCurrentOrDefault () { 15 | return this.languageGetter() || this.config.defaultLanguage; 16 | } 17 | 18 | /** 19 | * Logs warnings into console if the translation contains some unreplaced variable 20 | * @return {void} 21 | */ 22 | _logUnreplacedVars (vars, path) { 23 | if (!this.config.supressWarnings) { 24 | console.warn('VueLocalize. Unreplaced: ' + join(vars, ', ') + ' in "' + path + '"') 25 | } 26 | } 27 | 28 | /** 29 | * Injects variables values into already translated string by 30 | * replcaing their string keys with their real values 31 | * 32 | * @return {String} 33 | */ 34 | _processVariables (translation, vars, path) { 35 | const VARIABLES_REGEXP = /%[a-z]*%/g 36 | const arrVars = translation.match(VARIABLES_REGEXP) 37 | if (!arrVars) { 38 | return translation 39 | } 40 | 41 | if (!vars) { 42 | this._logUnreplacedVars(arrVars, path) 43 | return translation 44 | } 45 | 46 | each(vars, function (value, key) { 47 | translation = replace(translation, key, value) 48 | }) 49 | 50 | const unreplacedVars = translation.match(VARIABLES_REGEXP) 51 | if (unreplacedVars) { 52 | this._logUnreplacedVars(unreplacedVars, path) 53 | } 54 | 55 | return translation 56 | } 57 | 58 | /** 59 | * Translate message 60 | * 61 | * @param message 62 | * @param params 63 | * @param lang 64 | * @returns {String} 65 | */ 66 | translate (message, params = null, lang = null) { 67 | if (!lang) lang = this.getCurrentOrDefault() 68 | const phrasePathParts = split(message, '.') 69 | const isGlobal = phrasePathParts.length === 1 70 | const exactPath = isGlobal ? this.config.defaultContextName + '.' + message : message 71 | if (!has(this.config.translations, exactPath)) { 72 | if (!this.config.supressWarnings) { 73 | console.warn('[VueLocalize]. Undefined path: "' + exactPath + '"') 74 | } 75 | return exactPath 76 | } 77 | 78 | const translationPath = exactPath + '.' + lang 79 | 80 | const isTranslationExists = has(this.config.translations, translationPath) 81 | if (isTranslationExists) { 82 | const translationExpected = get(this.config.translations, translationPath) 83 | return this._processVariables(translationExpected, params, translationPath) 84 | } 85 | 86 | if (!this.config.fallbackOnNoTranslation) { 87 | if (!this.config.supressWarnings) { 88 | console.warn('[VueLocalize]. Undefined translation: "' + translationPath + '"') 89 | } 90 | return exactPath 91 | } 92 | 93 | const fallbackTranslationPath = exactPath + '.' + this.config.fallbackLanguage 94 | const isFallbackTranslationExists = has(this.config.translations, fallbackTranslationPath) 95 | 96 | if (lang === this.config.fallbackLanguage || !isFallbackTranslationExists) { 97 | if (!this.config.supressWarnings) { 98 | console.warn('[VueLocalize]. Undefined FALLBACK translation: "' + fallbackTranslationPath + '"') 99 | } 100 | return exactPath 101 | } 102 | 103 | const translationFallback = get(this.config.translations, fallbackTranslationPath) 104 | return this._processVariables(translationFallback, params, fallbackTranslationPath) 105 | } 106 | } -------------------------------------------------------------------------------- /src/libs/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 25/03/16. 3 | */ 4 | 5 | export function has (object, key) { 6 | let keys = key.split('.') 7 | let result = object; 8 | keys.forEach((k) => { 9 | if (result) { 10 | result = result[k] 11 | } 12 | }) 13 | return typeof result !== 'undefined' 14 | } -------------------------------------------------------------------------------- /src/vue-localize-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 25/03/16. 3 | */ 4 | 5 | import {Translator} from './libs/translate' 6 | 7 | export const localizeVueDirective = (translator) => { 8 | return { 9 | translator: translator, 10 | deep: true, 11 | 12 | /** 13 | * Bind watcher for update translation on language changing 14 | */ 15 | bind: function () { 16 | const vm = this.vm 17 | this.unwatch = vm.$watch('$store.state.vueLocalizeVuexStoreModule.currentLanguage', bind(this.updateContent, this)) 18 | }, 19 | 20 | /** 21 | * Unbind watcher 22 | */ 23 | unbind: function () { 24 | this.unwatch && this.unwatch() 25 | }, 26 | 27 | /** 28 | * Render element with directive 29 | */ 30 | update: function (target) { 31 | this.path = target.path 32 | this.vars = target.vars 33 | this.lang = target.lang 34 | var translateTo = target.lang || this.translator.getCurrentOrDefault() 35 | var translation = this.translator.translate(target.path, target.vars, translateTo) 36 | this.el.innerHTML = translation 37 | }, 38 | 39 | /** 40 | * Update element innerHTML on language changing 41 | */ 42 | updateContent: function (newLang) { 43 | var translateTo = this.lang || newLang 44 | var translation = this.translator.translate(this.path, this.vars, translateTo) 45 | this.el.innerHTML = translation 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/vue-localize.js: -------------------------------------------------------------------------------- 1 | import { kebabCase, each, set, unset, clone, cloneDeep } from 'lodash' 2 | import { currentLanguage } from './vuex-getters' 3 | import { localizeVueDirective } from './vue-localize-directive' 4 | import { has } from './libs/utils' 5 | 6 | // @todo by Saymon: pick out into config 7 | var localStorageKey = 'currentLanguage' 8 | 9 | function saveLanguageToLocalStorage (lang) { 10 | window.localStorage.setItem(localStorageKey, lang) 11 | } 12 | 13 | function getFromLocalStorage () { 14 | return window.localStorage.getItem(localStorageKey) 15 | } 16 | 17 | // 18 | // ****************************************************************** 19 | // VUEX STORE MODULE // 20 | // ****************************************************************** 21 | // 22 | 23 | /** 24 | * Mutation for switching applcation language 25 | */ 26 | const SET_APP_LANGUAGE = 'SET_APP_LANGUAGE' 27 | 28 | const state = { 29 | currentLanguage: null 30 | } 31 | 32 | const mutations = { 33 | /** 34 | * @state {Object} 35 | * @lang {String} 36 | */ 37 | [SET_APP_LANGUAGE] (state, lang, saveToLocalStorage = true) { 38 | state.currentLanguage = lang 39 | if (saveToLocalStorage) { 40 | saveLanguageToLocalStorage(lang) 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Export Vuex store module 47 | */ 48 | export const vueLocalizeVuexStoreModule = { 49 | state, 50 | mutations 51 | } 52 | 53 | // 54 | // ****************************************************************** 55 | // PLUGIN // 56 | // ****************************************************************** 57 | // 58 | 59 | /** 60 | * @Vue {Object} - Vue 61 | * @options {Object} - plugin options 62 | */ 63 | export default function install (Vue, options) { 64 | /** 65 | * @store {Object} - an instance of a vuex storage 66 | * @config {Object} - config object 67 | * @routesRegistry {Object} - registry of a routes (with initial names and localized names) 68 | */ 69 | const { store, config, router, routes } = options 70 | 71 | Vue.mixin({ 72 | vuex: { 73 | getters: { 74 | currentLanguage: currentLanguage 75 | } 76 | } 77 | }) 78 | 79 | store.dispatch('SET_APP_LANGUAGE', config.defaultLanguage, false) 80 | 81 | var idIncrement = 0 82 | var routesComponents = {} 83 | var routesRegistry = {initial: {}, localized: {}} 84 | 85 | /** 86 | * Returns current selected application language 87 | * @return {String} 88 | */ 89 | function _currentLanguage () { 90 | return store.state.vueLocalizeVuexStoreModule.currentLanguage 91 | } 92 | 93 | /** 94 | * Recursive renaming subroutes 95 | */ 96 | function _localizeSubroutes (subroutes, lang, routesRegistry) { 97 | each(subroutes, function (route, path) { 98 | if (has(route, 'name')) { 99 | set(routesRegistry.initial, route.name, path) 100 | route.originalName = route.name 101 | route.name = lang + '_' + route.name 102 | set(routesRegistry.localized, route.name, lang) 103 | } 104 | 105 | if (has(route, 'subRoutes')) { 106 | var objSubs = clone(route.subRoutes) 107 | unset(route, 'subRoutes') 108 | 109 | var subs = cloneDeep(objSubs) 110 | var subroutesLocalized = _localizeSubroutes(subs, lang, routesRegistry) 111 | route.subRoutes = subroutesLocalized 112 | } 113 | }) 114 | 115 | return subroutes 116 | } 117 | 118 | /** 119 | * Recursive action call 120 | */ 121 | function _recursively (object, action, isRoot = true) { 122 | each(object, function (value, key) { 123 | if (isRoot === true && !has(value, 'localized')) { 124 | return 125 | } 126 | 127 | action(key, value) 128 | if (has(value, 'subRoutes')) { 129 | _recursively(value.subRoutes, action, false) 130 | } 131 | }) 132 | } 133 | 134 | /** 135 | * Assign route id 136 | */ 137 | function _identicateRoutes (path, routeConfig) { 138 | set(routeConfig, 'vueLocalizeId', idIncrement) 139 | idIncrement++ 140 | } 141 | 142 | /** 143 | * Add component into separate object by previously assigned route id 144 | */ 145 | function _collectComponents (path, routeConfig) { 146 | set(routesComponents, routeConfig.vueLocalizeId, {}) 147 | set(routesComponents[routeConfig.vueLocalizeId], 'component', routeConfig.component) 148 | } 149 | 150 | /** 151 | * Detach component from route 152 | */ 153 | function _detachComponents (path, routeConfig) { 154 | unset(routeConfig, 'component') 155 | } 156 | 157 | function _attachComponents (path, routeConfig) { 158 | set(routeConfig, 'component', routesComponents[routeConfig.vueLocalizeId].component) 159 | } 160 | 161 | /** 162 | * Localization of routes 163 | */ 164 | function localizeRoutes (routes, config) { 165 | _recursively(routes, _identicateRoutes) 166 | _recursively(routes, _collectComponents) 167 | _recursively(routes, _detachComponents) 168 | 169 | each(routes, function (routeConfig, path) { 170 | if (!has(routeConfig, 'localized')) { 171 | return 172 | } 173 | 174 | if (has(routeConfig, 'name')) { 175 | set(routesRegistry.initial, routeConfig.name, path) 176 | } 177 | 178 | var objRoute = clone(routeConfig) 179 | unset(routes, path) 180 | 181 | if (has(objRoute, 'subRoutes')) { 182 | var objSubs = clone(objRoute.subRoutes) 183 | unset(objRoute, 'subRoutes') 184 | } 185 | 186 | each(config.languages, function (langConfig, lang) { 187 | if (!langConfig.enabled) { 188 | return 189 | } 190 | 191 | var newNode = clone(objRoute) 192 | var suffix = '' 193 | if (path[0] === '/' && path.length === 1) { 194 | suffix = '' 195 | } else if (path[0] === '/' && path.length > 1) { 196 | suffix = path 197 | } else if (path[0] !== '/') { 198 | suffix = '/' + path 199 | } 200 | 201 | var prefix = lang 202 | if (config.defaultLanguageRoute === false) { 203 | prefix = config.defaultLanguage !== lang ? lang : '' 204 | } 205 | 206 | var newPath = '/' + prefix + suffix 207 | newNode.lang = lang 208 | 209 | var subs = cloneDeep(objSubs) 210 | var subroutesLocalized = _localizeSubroutes(subs, lang, routesRegistry) 211 | newNode.subRoutes = subroutesLocalized 212 | set(routes, newPath, newNode) 213 | }) 214 | }) 215 | 216 | _recursively(routes, _attachComponents) 217 | 218 | return routes 219 | } 220 | 221 | var routesMap = localizeRoutes(routes, config) 222 | router.map(routesMap) 223 | 224 | router.beforeEach(function (transition) { 225 | if (transition.to.localized) { 226 | /* prevent unnecessary mutation call */ 227 | if (_currentLanguage() !== transition.to.lang) { 228 | store.dispatch('SET_APP_LANGUAGE', transition.to.lang, config.resaveOnLocalizedRoutes) 229 | } 230 | } else if (transition.from.localized === true && !config.resaveOnLocalizedRoutes) { 231 | // Restore memorized language from local storage for not localized routes 232 | var localStoredLanguage = getFromLocalStorage() 233 | if (localStoredLanguage && /* prevent unnecessary mutation call */ transition.from.lang !== localStoredLanguage) { 234 | store.dispatch('SET_APP_LANGUAGE', localStoredLanguage, false) 235 | } 236 | } 237 | 238 | transition.next() 239 | }) 240 | 241 | /** 242 | * Object with VueLocalize config 243 | */ 244 | Vue.prototype['$localizeConf'] = config 245 | 246 | Vue.prototype['$vueLocalizeInit'] = (route) => { 247 | var initialLanguage = has(route, 'localized') ? route.lang : getFromLocalStorage() 248 | if (initialLanguage) { 249 | store.dispatch('SET_APP_LANGUAGE', initialLanguage, config.resaveOnLocalizedRoutes) 250 | } 251 | } 252 | 253 | /** 254 | * Localize route name by adding prefix (e.g. 'en_') with language code. 255 | */ 256 | Vue.prototype['$localizeRoute'] = (name, lang = null) => { 257 | if (!has(routesRegistry.initial, name)) { 258 | return name 259 | } 260 | 261 | var prefix = (lang || _currentLanguage()) + '_' 262 | return prefix + name 263 | } 264 | 265 | Vue.prototype['$localizeRoutePath'] = (route, newLang) => { 266 | var path = route.path 267 | var name = route.name 268 | 269 | if (!has(routesRegistry.initial, name) && !has(routesRegistry.localized, name)) { 270 | return path 271 | } 272 | 273 | if (config.defaultLanguageRoute === true) { 274 | return path.replace(/^.{3}/g, '/' + newLang) 275 | } 276 | 277 | if (config.defaultLanguage === _currentLanguage()) { 278 | return '/' + newLang + path 279 | } 280 | 281 | if (newLang === config.defaultLanguage) { 282 | var newPath = path.replace(/^.{3}/g, '') 283 | if (!newPath.length) { 284 | newPath = '/' 285 | } 286 | 287 | return newPath 288 | } 289 | } 290 | 291 | Vue.prototype['$isJustLanguageSwitching'] = (transition) => { 292 | return transition.from.originalName === transition.to.originalName 293 | } 294 | 295 | const translator = new Translator(config, _currentLanguage) 296 | const translate = translator.translate 297 | // Adding global filter and global method $translate 298 | each({ translate }, function (helper, name) { 299 | Vue.filter(kebabCase(name), helper) 300 | Vue.prototype['$' + name] = helper 301 | }) 302 | 303 | // Adding directive 304 | Vue.directive('localize', localizeVueDirective(translator)) 305 | } 306 | -------------------------------------------------------------------------------- /src/vuex-getters.js: -------------------------------------------------------------------------------- 1 | export const currentLanguage = (state) => { 2 | return state.vueLocalizeVuexStoreModule.currentLanguage 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true 4 | } 5 | } -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | // Polyfill fn.bind() for PhantomJS 5 | /* eslint-disable no-extend-native */ 6 | Function.prototype.bind = require('function-bind') 7 | var entries = require('object.entries') 8 | if (!Object.entries) { 9 | entries.shim() 10 | } 11 | // require all test files (files that ends with .spec.js) 12 | var testsContext = require.context('./specs', true, /\.spec$/) 13 | testsContext.keys().forEach(testsContext) 14 | 15 | // require all src files except main.js for coverage. 16 | // you can also change this to match only the subset of files that 17 | // you want coverage for. 18 | var srcContext = require.context('../../src', true, /^\.\/(?!vue\-localize(\.js)?$)/) 19 | srcContext.keys().forEach(srcContext) -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var path = require('path') 7 | var merge = require('webpack-merge') 8 | var baseConfig = require('../../build/webpack.config') 9 | 10 | var webpackConfig = merge(baseConfig, { 11 | // use inline sourcemap for karma-sourcemap-loader 12 | vue: { 13 | loaders: { 14 | js: 'isparta' 15 | } 16 | } 17 | }) 18 | 19 | // no need for app entry during tests 20 | delete webpackConfig.entry 21 | 22 | module.exports = function (config) { 23 | config.set({ 24 | browsers: ['PhantomJS'], 25 | frameworks: ['jasmine'], 26 | reporters: ['spec', 'coverage'], 27 | files: ['./index.js'], 28 | preprocessors: { 29 | './index.js': ['webpack', 'coverage'] 30 | }, 31 | webpack: webpackConfig, 32 | webpackMiddleware: { 33 | noInfo: true 34 | }, 35 | coverageReporter: { 36 | dir: './coverage', 37 | reporters: [ 38 | { type: 'lcov', subdir: '.' }, 39 | { type: 'text-summary' } 40 | ] 41 | } 42 | }) 43 | } -------------------------------------------------------------------------------- /test/unit/specs/utils/_data/vue-localize-conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import localization file with translations 3 | */ 4 | import translations from './vue-localize-translations' 5 | 6 | export default { 7 | /** 8 | * The list of application languages 9 | */ 10 | languages: { 11 | en: { 12 | key: 'eng', // phrase key in the translation file for translating language name into different languages 13 | enabled: true // Set to false if need to disable language 14 | }, 15 | ru: { 16 | key: 'rus', 17 | enabled: true 18 | }, 19 | ge: { 20 | key: 'geo', 21 | enabled: false 22 | } 23 | }, 24 | 25 | /** 26 | * Default language will be used if the language nowhere defined (together in current route, Vuex store and localStorage) 27 | * Usually it works if user came for the first time 28 | */ 29 | defaultLanguage: 'en', 30 | 31 | /** 32 | * The object with translations of application phrases 33 | */ 34 | translations: translations, 35 | 36 | /** 37 | * Defines the behaviour of routes localization (adding language at the start of path of a routes) 38 | * If false, disable leading language param for all routes with default language, or enable otherwise 39 | */ 40 | defaultLanguageRoute: false, 41 | 42 | /** 43 | * Defines the policy for storing selected language in browser local storage for 44 | * transiotions from localized routes to not localized and backwards 45 | * 46 | * If false, transition from NOT localized route to localized will not update selected language in local storage, and 47 | * it will be taken up when you'll back TO NOT localized route FROM LOCALIZED, even you have switched languages with 48 | * language selector. 49 | * It can be useful in cases when you need to remember the language selected in user account or administrative panel 50 | * and switching languages at the public section of a site should not affect this choice 51 | * 52 | * Set to true if you need transparent behaviour of application when switching languages and language must be changed for 53 | * all application regargless of where exactly it was switched, in administration panel or at the public section of a site 54 | */ 55 | resaveOnLocalizedRoutes: false, 56 | 57 | /** 58 | * Name of the key for default context of translations 59 | */ 60 | defaultContextName: 'global', 61 | 62 | /** 63 | * Set to true if you want to translate phrases which has no translation 64 | * into the language defined in the option "fallbackLanguage" below 65 | * It may be usefull when you already need to publish your app, but you have 66 | * no complete translations into all languages for all phrases 67 | */ 68 | fallbackOnNoTranslation: false, 69 | 70 | /** 71 | * Defines the fallback language for using in case described in comment for the option above 72 | */ 73 | fallbackLanguage: 'en', 74 | 75 | /** 76 | * Suppress warnings emitted into browser console (concerns only translation process) 77 | * Plugin can emit warnings during translation phrases process in the following cases: 78 | * 79 | * 1) phrase path doesn't exists in localization file (emitted always) 80 | * 81 | * 2) phrase path exists but there is no translation into current 82 | * language (emitted only if "fallbackOnNoTranslation" is set to false) 83 | * 84 | * 3) phrase path exists, hasn't translation into current language and hasn't 85 | * translation into fallback language (emitted only if "fallbackOnNoTranslation" is 86 | * set to true) 87 | * 88 | * 4) Output translation contains unprocessed variables which will shown to user as is, e.g. %foo% 89 | */ 90 | supressWarnings: false 91 | } 92 | -------------------------------------------------------------------------------- /test/unit/specs/utils/_data/vue-localize-translations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | // ------------------------------------------------------------------------- GLOBAL 4 | 'global': { 5 | 6 | projectName: { 7 | en: 'VueJS SPA sample', 8 | ru: 'Шаблон VueJS SPA' 9 | }, 10 | 11 | defaultTitle: { 12 | en: '*en*', 13 | ru: '*ru*' 14 | }, 15 | 16 | // ----------------------------------------------------------------------- GLOBAL . LANG 17 | lang: { 18 | eng: { 19 | en: 'English', 20 | ru: 'Английский' 21 | }, 22 | rus: { 23 | en: 'Russian', 24 | ru: 'Русский' 25 | } 26 | } 27 | 28 | }, 29 | 30 | // ------------------------------------------------------------------------- PUBPLIC 31 | public: { 32 | 33 | // ----------------------------------------------------------------------- PUBPLIC . HEADER 34 | header: { 35 | nav: { 36 | home: { 37 | en: 'Home', 38 | ru: 'Главная' 39 | }, 40 | about: { 41 | en: 'About', 42 | ru: 'О проекте' 43 | }, 44 | features: { 45 | en: 'Features', 46 | ru: 'Возможности' 47 | } 48 | }, 49 | signIn: { 50 | en: 'Sign In', 51 | ru: 'Вход' 52 | }, 53 | signUp: { 54 | en: 'Sign Up', 55 | ru: 'Регистрация' 56 | }, 57 | account: { 58 | en: 'Account', 59 | ru: 'Кабинет' 60 | }, 61 | unauth: { 62 | en: 'Without authentication', 63 | ru: 'Без аутентификации' 64 | } 65 | }, 66 | 67 | // ----------------------------------------------------------------------- PUBPLIC . INDEX 68 | index: { 69 | title: { 70 | en: 'VueJS SPA Sample', 71 | ru: 'Шаблон SPA на VueJS' 72 | }, 73 | jdesc: { 74 | en: 'This is an index page of the VueJS SPA boilerplate', 75 | ru: 'Это главная страница шаблона одностраничного приложения на Vue.js' 76 | }, 77 | jp1: { 78 | en: 'This boilerplate implements such features as authentication, localization, routing, and other basic things, which are so essential in almost all modern applications', 79 | ru: 'В шаблоне реализованы такие функции как аутентификация, поддержка мультиязычности, роутинг, и многие другие базовые механизмы, необходимые практически во всех современных приложениях' 80 | }, 81 | jp2: { 82 | en: 'Complete list of the features and ways of implementation %features_link%', 83 | ru: 'С подробным списком функций и предложенными вариантами реализации можно ознакомиться в разделе %features_link%' 84 | }, 85 | jp3: { 86 | en: 'Click button below to login', 87 | ru: 'Воспользуйтесь кнопкой ниже чтобы перейти к форме входа' 88 | }, 89 | jbtn: { 90 | en: 'Sign In', 91 | ru: 'Войти' 92 | } 93 | }, 94 | 95 | // ----------------------------------------------------------------------- PUBPLIC . ABOUT 96 | about: { 97 | title: { 98 | en: 'About this sample', 99 | ru: 'Об этом шаблоне' 100 | } 101 | }, 102 | 103 | // ----------------------------------------------------------------------- PUBPLIC . FEATURES 104 | features: { 105 | title: { 106 | en: 'Features', 107 | ru: 'Возможности' 108 | } 109 | }, 110 | 111 | // ----------------------------------------------------------------------- PUBPLIC . ERROR_404 112 | err404: { 113 | title: { 114 | en: '404 - Not page found', 115 | ru: '404 - Страница не найдена' 116 | } 117 | }, 118 | 119 | // ----------------------------------------------------------------------- PUBPLIC . ERROR_404 120 | signIn: { 121 | title: { 122 | en: 'Sign in', 123 | ru: 'Вход' 124 | } 125 | }, 126 | 127 | // ----------------------------------------------------------------------- PUBPLIC . FEATURES_LIST 128 | ftr: { 129 | appStructure: { 130 | en: 'Application structure', 131 | ru: 'Структура приложения' 132 | }, 133 | routing: { 134 | en: 'Routing', 135 | ru: 'Роутинг', 136 | 137 | transition: { 138 | en: 'Performing transition between pages vie VueRouter', 139 | ru: 'Осуществление переходов между страницами при помощи VueRouter' 140 | }, 141 | page404error: { 142 | en: '404 error pages and redirecting', 143 | ru: 'Страницы 404 ошибки и редиректы' 144 | }, 145 | navHighlighting: { 146 | en: 'Nav menu highlighting', 147 | ru: 'Подсветка навигационных меню' 148 | }, 149 | titleChanging: { 150 | en: 'Page title changing (considering current language)', 151 | ru: 'Смена title страницы при при переходе (должно учитывать текущий язык)' 152 | }, 153 | authRequiredCheck: { 154 | en: 'Check is it possible to go to the page which requires authentication. Redirect to login if no.', 155 | ru: 'Проверка возможности перехода к странице, требующей аутентификации. Редирект на форму входа при необходимости.' 156 | }, 157 | goOutOpportunityCheck: { 158 | // en: '', 159 | ru: 'Проверка возможности ухода со страницы, например если есть какие-то незавершенные процессы или другие ситуации при которых нежелательно покидать страницу. Механизмы блокировки перехода и нотификации о нежелательном уходе' 160 | }, 161 | languagePartInRoute: { 162 | en: 'Including language into route URI for indexing public section by searchengines', 163 | ru: 'Включение языкового параметра в URI роута для индексации поисковиками публичной части веб-проекта' 164 | }, 165 | loadingDataFromServer: { 166 | en: 'Loading data for the page from server', 167 | ru: 'Загрузка данных с сервера при переходе на страницу' 168 | } 169 | }, 170 | multilingual: { 171 | en: 'Multilingual', 172 | ru: 'Мультиязычность', 173 | 174 | plugin: { 175 | en: 'Wrapping localization support in a separate plugin "vue-localize" for VueJS', 176 | ru: 'Обертка поддержки мультиязычности в плагин "vue-localize" для VueJS' 177 | }, 178 | languageSelector: { 179 | en: 'Language selector', 180 | ru: 'Селектор языка' 181 | }, 182 | currLangAsVuexState: { 183 | en: 'Storing current language as an application-level state in a Vuex store', 184 | ru: 'Хранение текущего языка в хранилище состояний приложения Vuex' 185 | }, 186 | fallbackLanguage: { 187 | en: 'Defining a fallback language for using if no translation', 188 | ru: 'Определение fallback языка для использования в случае отсутствия перевода на выбраный' 189 | }, 190 | rememberInLocalStorage: { 191 | en: 'Remember selected language in a local storage of a browser', 192 | ru: 'Запоминание выбранного языка в локальном хранилище браузера' 193 | }, 194 | currLangVuexGetter: { 195 | en: 'Getting current language inside vue component', 196 | ru: 'Получение кода текущего языка внутри компонеты Vue' 197 | }, 198 | useFitler: { 199 | en: 'Translating phrases with a Vue filter', 200 | ru: 'Перевод фраз при помощи фильтра Vue' 201 | }, 202 | useDirectCall: { 203 | en: 'Translating phrases with a direct call of plugin method', 204 | ru: 'Перевод фраз при помощи вызова метода плагина' 205 | }, 206 | useDirective: { 207 | en: 'Translating phrases with a directive', 208 | ru: 'Перевод фраз при помощи директивы' 209 | }, 210 | injectionVariables: { 211 | en: 'Injection passed variables into translation', 212 | ru: 'Внедрение в перевод значений переданных переменных' 213 | }, 214 | reactivePageTitleTranslation: { 215 | en: 'Reactive page title translation on language changing', 216 | ru: 'Реактивный перевод тайтла страницы при смене языка' 217 | }, 218 | vueLocalizeNpm: { 219 | en: 'Pick out the VueLocalize plugin in a separate NPM package', 220 | ru: 'Вынести плагин VueLocalize в отдельный NPM пакет' 221 | } 222 | }, 223 | authentication: { 224 | en: 'Authentication', 225 | ru: 'Аутентификация' 226 | }, 227 | connectionCheck: { 228 | en: 'Internet connection control', 229 | ru: 'Контроль интернет соединения' 230 | }, 231 | localStorageAndVuex: { 232 | en: 'Local storage + Vuex', 233 | ru: 'Local storage + Vuex' 234 | }, 235 | apiRequestsQueue: { 236 | en: 'API requests queue', 237 | ru: 'Очередь запросов к API' 238 | }, 239 | webSockets: { 240 | en: 'WebSockets', 241 | ru: 'Веб-сокеты' 242 | }, 243 | apiClient: { 244 | en: 'API Client', 245 | ru: 'API клиент' 246 | } 247 | } 248 | }, 249 | 250 | // ------------------------------------------------------------------------- ACCOUNT 251 | account: { 252 | 253 | // ----------------------------------------------------------------------- ACCOUNT . NAV 254 | nav: { 255 | home: { 256 | en: 'Home', 257 | ru: 'Главная' 258 | }, 259 | profile: { 260 | en: 'Profile', 261 | ru: 'Профайл' 262 | }, 263 | help: { 264 | en: 'Help', 265 | ru: 'Помощь' 266 | } 267 | }, 268 | 269 | // ----------------------------------------------------------------------- ACCOUNT . HOME 270 | home: { 271 | title: { 272 | en: 'Dashboard', 273 | ru: 'Обзор' 274 | }, 275 | helloText: { 276 | en: 'Hello! This is your account dashboard.', 277 | ru: 'Привет! Это гавная страница личного кабинета.' 278 | } 279 | }, 280 | 281 | // ----------------------------------------------------------------------- ACCOUNT . PROFILE 282 | profile: { 283 | title: { 284 | en: 'User profile', 285 | ru: 'Профайл пользователя' 286 | } 287 | }, 288 | 289 | // ----------------------------------------------------------------------- ACCOUNT . HELP 290 | help: { 291 | title: { 292 | en: 'Help', 293 | ru: 'Помощь' 294 | }, 295 | variablesInjectionTest: { 296 | en: 'Some text in English, %foo%, and then the %bar% variable', 297 | ru: 'А теперь некоторый текст на русском, переменная %bar% и затем переменная %foo%' 298 | } 299 | } 300 | 301 | }, 302 | 303 | // Features stauses 304 | fts: { 305 | backlog: { 306 | en: 'Backlog', 307 | ru: 'В очереди' 308 | }, 309 | inwork: { 310 | en: 'In work', 311 | ru: 'В работе' 312 | }, 313 | done: { 314 | en: 'Done!', 315 | ru: 'Готово!' 316 | } 317 | } 318 | 319 | } 320 | -------------------------------------------------------------------------------- /test/unit/specs/utils/has.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | 5 | import { has } from './../../../../src/libs/utils' 6 | 7 | describe('has.util', () => { 8 | it('should check if value exists by key with dot notation', () => { 9 | const obj1 = { 10 | a: { 11 | b: { 12 | c: null 13 | } 14 | } 15 | } 16 | expect(has(obj1, 'a.b.c')).toBeTruthy() 17 | 18 | const obj2 = { 19 | a2: { 20 | b2: { 21 | c2: null 22 | } 23 | } 24 | } 25 | expect(has(obj2, 'a.b.c')).toBeFalsy() 26 | 27 | }) 28 | }) -------------------------------------------------------------------------------- /test/unit/specs/utils/recursively.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | //import { _recursively } from './../../../../src/libs/utils' 5 | 6 | describe('Recursively util', () => { 7 | it('Should recursively call action', () => { 8 | 9 | expect().toBeUndefined() 10 | }) 11 | }) -------------------------------------------------------------------------------- /test/unit/specs/utils/replace.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | import { replace } from 'lodash' 5 | 6 | describe('Replace string function', () => { 7 | it('Should replace substring to replacement in the passed string', () => { 8 | const path = '/en/some_route_path' 9 | const replace_pattern = /^.{3}/g 10 | const replacement = '/ru' 11 | expect(path.replace(replace_pattern, replacement)) 12 | .toBe(replace(path, replace_pattern, replacement)) 13 | }) 14 | }) -------------------------------------------------------------------------------- /test/unit/specs/utils/translate.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by vestnik on 26/03/16. 3 | */ 4 | 5 | import { Translator } from './../../../../src/libs/translate' 6 | import config from './_data/vue-localize-conf' 7 | 8 | const languageGetterDefaultEn = () => 'en' 9 | const languageGetterDefaultRu = () => 'ru' 10 | 11 | describe('Translate', () => { 12 | it('Should translate string', () => { 13 | const t = new Translator(config, languageGetterDefaultEn) 14 | 15 | expect(t.translate('projectName', null)).toBe('VueJS SPA sample') 16 | expect(t.translate('projectName', null, 'en')).toBe('VueJS SPA sample') 17 | expect(t.translate('projectName', null, 'ru')).toBe('Шаблон VueJS SPA') 18 | 19 | t.languageGetter = languageGetterDefaultRu 20 | expect(t.translate('projectName', null)).toBe('Шаблон VueJS SPA') 21 | expect(t.translate('projectName', null, 'en')).toBe('VueJS SPA sample') 22 | expect(t.translate('projectName', null, 'ru')).toBe('Шаблон VueJS SPA') 23 | }) 24 | }) --------------------------------------------------------------------------------