├── .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 |
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 |
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 |
412 |
413 | {{ currentLanguage | uppercase }}
414 |
422 |
423 |
424 |
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 | {{ 'site.header.nav.home' | translate }}
481 | ```
482 |
483 | Translating into exact language, e.g. English
484 | ```html
485 | {{ 'site.header.nav.home' | translate null 'en' }}
486 | ```
487 |
488 | #### Translating via direct method call
489 |
490 | Translating into current language
491 | ```html
492 | {{ $translate('site.header.nav.home') }}
493 | or
494 |
495 | ```
496 |
497 | Translating into exact language, e.g. English
498 | ```html
499 | {{ $translate('site.header.nav.home', null, 'en') }}
500 | ```
501 |
502 | #### Translating via ```v-localize``` directive
503 |
504 | Translating into current language
505 | ```html
506 |
507 | ```
508 |
509 | Translating into exact language, e.g. English
510 | ```html
511 |
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 |
559 | ```
560 | #### Injecting with directive
561 | ```html
562 |
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 | })
--------------------------------------------------------------------------------