├── .browserslistrc
├── src
├── lang
│ ├── localized-urls
│ │ ├── en.json
│ │ ├── index.js
│ │ └── cs.json
│ ├── translations
│ │ ├── index.js
│ │ ├── en.json
│ │ └── cs.json
│ └── dateTimeFormats.js
├── assets
│ ├── cs.png
│ ├── en.png
│ └── logo.png
├── views
│ ├── PosInfo.vue
│ ├── PosManager.vue
│ ├── User.vue
│ ├── Home.vue
│ └── About.vue
├── main.js
├── components
│ └── HelloWorld.vue
├── plugin
│ ├── components
│ │ ├── LocalizedLink.vue
│ │ └── LanguageSwitcher.vue
│ └── index.js
├── router
│ └── index.js
└── App.vue
├── jest.config.js
├── lang-router.gif
├── public
├── favicon.ico
└── index.html
├── tests
├── setup
│ ├── Test.vue
│ ├── localized-urls.js
│ ├── routes.js
│ ├── i18nOptions.js
│ └── translations.js
└── unit
│ └── LangRouter.spec.js
├── .stylelintrc.js
├── rollup.banner.txt
├── babel.config.js
├── .gitignore
├── helpers
└── spread-polyfill.js
├── vue.config.js
├── .eslintrc.js
├── LICENSE
├── rollup.config.js
├── package.json
├── dist
├── lang-router.min.js
├── lang-router.esm.js
├── lang-router.js
└── lang-router.umd.js
└── README.md
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/src/lang/localized-urls/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": "about"
3 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@vue/cli-plugin-unit-jest',
3 | };
4 |
--------------------------------------------------------------------------------
/lang-router.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbrosaci/vue-lang-router/HEAD/lang-router.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbrosaci/vue-lang-router/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/cs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbrosaci/vue-lang-router/HEAD/src/assets/cs.png
--------------------------------------------------------------------------------
/src/assets/en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbrosaci/vue-lang-router/HEAD/src/assets/en.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbrosaci/vue-lang-router/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/tests/setup/Test.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('test.content') }}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lang/localized-urls/index.js:
--------------------------------------------------------------------------------
1 | import en from './en.json';
2 | import cs from './cs.json';
3 |
4 | export default { en, cs };
5 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [ './node_modules/@radek-altof/lint-config/stylelint.json' ],
3 | rules: {},
4 | };
5 |
--------------------------------------------------------------------------------
/rollup.banner.txt:
--------------------------------------------------------------------------------
1 | vue-lang-router v<%= pkg.version %>
2 | (c) 2021 <%= pkg.author.name %>
3 | Released under the <%= pkg.license %> License.
--------------------------------------------------------------------------------
/src/views/PosInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('pos.info') }}
4 |
ID: {{ $route.params.id }}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'babel-plugin-rewire',
4 | ],
5 | presets: [
6 | '@vue/cli-plugin-babel/preset',
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/src/lang/localized-urls/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": "o-nas",
3 | "user": "uzivatel",
4 | "manager": "manazer",
5 | "pos": "pobocka",
6 | "replace-no-no": "Johnny"
7 | }
--------------------------------------------------------------------------------
/src/views/PosManager.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('pos.manager') }}
4 |
ID: {{ $route.params.id }}
5 |
6 |
Manager: {{ $route.params.name }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/lang/translations/index.js:
--------------------------------------------------------------------------------
1 | import en from './en.json';
2 |
3 | export default {
4 | en: {
5 | name: 'English',
6 | messages: en,
7 | },
8 | cs: {
9 | name: 'Česky',
10 | load: () => { return import('./cs.json'); },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App.vue';
3 | import router from './router';
4 | import { i18n } from './plugin';
5 |
6 | Vue.config.productionTip = false;
7 |
8 | new Vue({
9 | router,
10 | i18n,
11 | render: h => h(App),
12 | }).$mount('#app');
13 |
--------------------------------------------------------------------------------
/tests/setup/localized-urls.js:
--------------------------------------------------------------------------------
1 | export default {
2 | en: {
3 | about: 'about-replaced',
4 | },
5 | cs: {
6 | about: 'o-nas',
7 | user: 'uzivatel',
8 | info: 'detail',
9 | },
10 | ru: {
11 | info: 'informatsiya',
12 | slug: 'should-not-be-replaced',
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 |
4 | # local env files
5 | .env.local
6 | .env.*.local
7 |
8 | # Log files
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | # Editor directories and files
14 | .idea
15 | .vscode
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.sw?
21 |
--------------------------------------------------------------------------------
/src/lang/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "nav": {
3 | "home": "Home",
4 | "about": "About",
5 | "user": "User"
6 | },
7 | "hello": "Hello, sunshine!",
8 | "content": "This is content",
9 | "user-slug": "User in URL is: ",
10 | "pos": {
11 | "info": "Branch info",
12 | "manager": "Branch manager"
13 | }
14 | }
--------------------------------------------------------------------------------
/src/lang/translations/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "nav": {
3 | "home": "Domů",
4 | "about": "O nás",
5 | "user": "Uživatel"
6 | },
7 | "hello": "Čus bejku!",
8 | "content": "Tohle je obsah",
9 | "user-slug": "Uživatel v URL je: ",
10 | "pos": {
11 | "info": "Informace o pobočce",
12 | "manager": "Manažer pobočky"
13 | }
14 | }
--------------------------------------------------------------------------------
/src/views/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
19 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ greeting }}
4 |
{{ $t('content') }}
5 |
6 |
7 |
8 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/tests/setup/routes.js:
--------------------------------------------------------------------------------
1 | import Test from './Test.vue';
2 |
3 | export default [
4 | {
5 | path: '/about',
6 | name: 'About page',
7 | component: () => import('./Test.vue'),
8 | },
9 | {
10 | path: '/user/:slug',
11 | name: 'User page',
12 | component: {
13 | template: '',
14 | },
15 | children: [
16 | {
17 | path: 'info',
18 | name: 'User info',
19 | component: Test,
20 | },
21 | ],
22 | },
23 | ];
24 |
--------------------------------------------------------------------------------
/helpers/spread-polyfill.js:
--------------------------------------------------------------------------------
1 | function mergeObjects() {
2 | let targetObject = {};
3 |
4 | for (let i = 0; i < arguments.length; i++) {
5 | let sourceObject = arguments[i];
6 |
7 | for (let key in sourceObject) {
8 | if (Object.prototype.hasOwnProperty.call(sourceObject, key)) {
9 | targetObject[key] = sourceObject[key];
10 | }
11 | }
12 | }
13 |
14 | return targetObject;
15 | }
16 |
17 | const _spread = Object.assign || mergeObjects;
18 | export default _spread;
--------------------------------------------------------------------------------
/tests/setup/i18nOptions.js:
--------------------------------------------------------------------------------
1 | export const dateTimeFormats = {
2 | en: {
3 | long: {
4 | year: 'numeric',
5 | month: 'numeric',
6 | day: 'numeric',
7 | },
8 | },
9 | cs: {
10 | short: {
11 | weekday: 'long',
12 | hour: 'numeric',
13 | minute: 'numeric',
14 | },
15 | },
16 | };
17 |
18 | export const pluralizationRules = {
19 | en: () => 1,
20 | cs: choice => {
21 | if (choice === 0) { return 0; }
22 | else { return 1; }
23 | },
24 | ru: () => 2,
25 | };
26 |
--------------------------------------------------------------------------------
/tests/setup/translations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | en: {
3 | name: 'English',
4 | load: () => Promise.resolve({
5 | test: {
6 | content: 'This is an about page',
7 | },
8 | }),
9 | },
10 | cs: {
11 | name: 'Česky',
12 | load: () => Promise.resolve({
13 | test: {
14 | content: 'Tohle je stránka O nás',
15 | },
16 | }),
17 | },
18 | ru: {
19 | name: 'русский',
20 | load: () => Promise.resolve({
21 | test: {
22 | content: 'Это страница О нас',
23 | },
24 | }),
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/src/lang/dateTimeFormats.js:
--------------------------------------------------------------------------------
1 | export default {
2 | en: {
3 | short: {
4 | year: 'numeric',
5 | month: 'numeric',
6 | day: 'numeric',
7 | },
8 | long: {
9 | year: 'numeric',
10 | month: 'long',
11 | day: 'numeric',
12 | weekday: 'short',
13 | hour: 'numeric',
14 | minute: 'numeric',
15 | },
16 | },
17 | cs: {
18 | short: {
19 | weekday: 'long',
20 | hour: 'numeric',
21 | minute: 'numeric',
22 | },
23 | long: {
24 | year: 'numeric',
25 | month: 'long',
26 | day: 'numeric',
27 | },
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const StyleLintPlugin = require('stylelint-webpack-plugin');
2 |
3 | module.exports = {
4 | configureWebpack: {
5 | plugins: [
6 | new StyleLintPlugin({
7 | files: [ 'src/**/*.{vue,htm,html,css,sss,less,scss,sass}' ],
8 | fix: true,
9 | }),
10 | ],
11 | },
12 | runtimeCompiler: true,
13 | chainWebpack: config => {
14 | config.module.rule('eslint').use('eslint-loader').options({
15 | fix: true,
16 | });
17 | config.module.rule('i18n').resourceQuery(/blockType=i18n/).type('javascript/auto').use('i18n').loader('@kazupon/vue-i18n-loader').end();
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "cs": {
4 | "heading": "Tohle je stránka O nás."
5 | },
6 | "en": {
7 | "heading": "This is an About page."
8 | }
9 | }
10 |
11 |
12 |
13 |
14 |
{{ $t('heading') }}
15 |
{{ $t('content') }}
16 |
17 |
18 |
19 |
34 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | './node_modules/@radek-altof/lint-config/eslint.json',
9 | ],
10 | parserOptions: {
11 | parser: 'babel-eslint',
12 | },
13 | rules: {
14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
16 | 'vue/html-indent': [ 'error', 'tab' ],
17 | },
18 | overrides: [
19 | {
20 | files: [
21 | '**/__tests__/*.{j,t}s?(x)',
22 | '**/tests/unit/**/*.spec.{j,t}s?(x)',
23 | ],
24 | env: {
25 | jest: true,
26 | },
27 | },
28 | ],
29 | };
30 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/plugin/components/LocalizedLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Radek Altof
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/plugin/components/LanguageSwitcher.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
54 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import LangRouter from '../plugin';
3 | import Home from '../views/Home.vue';
4 |
5 | import translations from '../lang/translations';
6 | import localizedURLs from '../lang/localized-urls';
7 | import dateTimeFormats from '../lang/dateTimeFormats';
8 |
9 |
10 | Vue.use(LangRouter, {
11 | defaultLanguage: 'en',
12 | translations,
13 | localizedURLs,
14 | i18nOptions: {
15 | dateTimeFormats,
16 | },
17 | });
18 |
19 |
20 | const routes = [
21 | {
22 | path: '/',
23 | name: 'Home page',
24 | component: Home,
25 | alias: '/home',
26 | },
27 | {
28 | path: '/about',
29 | name: 'About page',
30 | // route level code-splitting
31 | // this generates a separate chunk (about.[hash].js) for this route
32 | // which is lazy-loaded when the route is visited.
33 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
34 | },
35 | {
36 | path: '/user/:slug',
37 | name: 'User page',
38 | component: () => import('../views/User.vue'),
39 | },
40 | {
41 | path: '/pos/:id',
42 | name: 'Pos page',
43 | component: {
44 | template: '',
45 | },
46 | children: [
47 | {
48 | path: 'info',
49 | name: 'Pos info',
50 | component: () => import('../views/PosInfo.vue'),
51 | },
52 | {
53 | path: 'manager/:name',
54 | name: 'Pos manager',
55 | component: () => import('../views/PosManager.vue'),
56 | },
57 | ],
58 | },
59 | ];
60 |
61 |
62 | // Initiate router
63 | const router = new LangRouter({
64 | routes,
65 | mode: 'history',
66 | });
67 |
68 | export default router;
69 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs'; // Convert CommonJS modules to ES6
2 | import vue from 'rollup-plugin-vue'; // Handle .vue SFC files
3 | import buble from '@rollup/plugin-buble'; // Transpile/polyfill with reasonable browser support
4 | import banner from 'rollup-plugin-banner'; // Add header to compiled JS files
5 | import { terser } from 'rollup-plugin-terser'; // Minification
6 | import cleanup from 'rollup-plugin-cleanup'; // Code cleanup
7 | import inject from '@rollup/plugin-inject'; // Import injection
8 |
9 | const path = require('path');
10 |
11 | const globals = {
12 | vue: 'Vue',
13 | 'vue-i18n': 'VueI18n',
14 | 'vue-router': 'VueRouter',
15 | };
16 |
17 | export default {
18 | input: 'src/plugin/index.js',
19 | external: [
20 | 'vue',
21 | 'vue-i18n',
22 | 'vue-router',
23 | ],
24 | output: [
25 | {
26 | file: 'dist/lang-router.umd.js',
27 | format: 'umd',
28 | name: 'LangRouter',
29 | exports: 'named',
30 | globals,
31 | },
32 | {
33 | file: 'dist/lang-router.esm.js',
34 | format: 'es',
35 | },
36 | {
37 | file: 'dist/lang-router.js',
38 | format: 'iife',
39 | name: 'LangRouter',
40 | exports: 'named',
41 | globals,
42 | },
43 | {
44 | file: 'dist/lang-router.min.js',
45 | format: 'iife',
46 | name: 'LangRouter',
47 | exports: 'named',
48 | globals,
49 | plugins: [ terser() ],
50 | },
51 | ],
52 | plugins: [
53 | commonjs(),
54 | vue({
55 | css: true, // Dynamically inject css as a
105 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-lang-router",
3 | "version": "1.3.1",
4 | "description": "Vue.js language routing with (optional) URL localization",
5 | "author": {
6 | "name": "Radek Altof",
7 | "email": "radek.altof@gmail.com"
8 | },
9 | "scripts": {
10 | "serve": "vue-cli-service serve",
11 | "build": "rollup --config",
12 | "test:unit": "vue-cli-service test:unit",
13 | "test": "npm run lint && npm run test:unit",
14 | "lint": "vue-cli-service lint"
15 | },
16 | "main": "dist/lang-router.umd.js",
17 | "module": "dist/lang-router.esm.js",
18 | "unpkg": "dist/lang-router.js",
19 | "files": [
20 | "dist/*.js"
21 | ],
22 | "dependencies": {
23 | "core-js": "^3.17.2",
24 | "vue": "^2.6.14",
25 | "vue-i18n": "^8.25.0",
26 | "vue-router": "^3.5.2"
27 | },
28 | "devDependencies": {
29 | "@kazupon/vue-i18n-loader": "^0.5.0",
30 | "@radek-altof/lint-config": "^1.1.1",
31 | "@rollup/plugin-buble": "^0.21.3",
32 | "@rollup/plugin-commonjs": "^20.0.0",
33 | "@rollup/plugin-inject": "^4.0.2",
34 | "@vue/cli-plugin-babel": "~4.5.13",
35 | "@vue/cli-plugin-eslint": "~4.5.13",
36 | "@vue/cli-plugin-router": "~4.5.13",
37 | "@vue/cli-plugin-unit-jest": "^4.5.13",
38 | "@vue/cli-service": "~4.5.13",
39 | "@vue/test-utils": "^1.2.2",
40 | "babel-eslint": "^10.1.0",
41 | "babel-plugin-rewire": "^1.2.0",
42 | "eslint": "^6.8.0",
43 | "eslint-plugin-vue": "^6.2.2",
44 | "git-branch-is": "^4.0.0",
45 | "node-sass": "^4.14.1",
46 | "rollup": "^2.56.3",
47 | "rollup-plugin-banner": "^0.2.1",
48 | "rollup-plugin-cleanup": "^3.2.1",
49 | "rollup-plugin-terser": "^7.0.2",
50 | "rollup-plugin-vue": "^5.1.9",
51 | "sass-loader": "^8.0.2",
52 | "stylelint": "^13.13.1",
53 | "stylelint-webpack-plugin": "^2.2.2",
54 | "vue-template-compiler": "^2.6.14"
55 | },
56 | "bugs": {
57 | "url": "https://github.com/adbrosaci/vue-lang-router/issues"
58 | },
59 | "homepage": "https://github.com/adbrosaci/vue-lang-router#readme",
60 | "jsdelivr": "dist/lang-router.js",
61 | "keywords": [
62 | "vue",
63 | "router",
64 | "routing",
65 | "lang",
66 | "languages",
67 | "i18n",
68 | "localization",
69 | "plugin"
70 | ],
71 | "license": "MIT",
72 | "repository": {
73 | "type": "git",
74 | "url": "git+https://github.com/adbrosaci/vue-lang-router.git"
75 | },
76 | "gitHooks": {
77 | "pre-commit": "if git-branch-is master -q; then npm run test && npm run build; fi"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/unit/LangRouter.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import LangRouter from '@/plugin';
4 |
5 | import routes from '../setup/routes';
6 | import translations from '../setup/translations';
7 | import localizedURLs from '../setup/localized-urls';
8 | import { dateTimeFormats, pluralizationRules } from '../setup/i18nOptions';
9 |
10 |
11 | // Setup
12 |
13 | const defaultLanguage = 'en';
14 |
15 | Vue.use(LangRouter, {
16 | defaultLanguage,
17 | translations,
18 | localizedURLs,
19 | i18nOptions: {
20 | dateTimeFormats,
21 | pluralizationRules,
22 | },
23 | });
24 |
25 | const router = new LangRouter({
26 | routes,
27 | mode: 'history',
28 | });
29 |
30 |
31 | // Tests
32 |
33 | describe('General', () => {
34 |
35 | test('translations are passed in correctly', () => {
36 | expect(LangRouter.__get__('translations')).toEqual(translations);
37 | });
38 |
39 | test('localizedURLs are passed in correctly', () => {
40 | expect(LangRouter.__get__('localizedURLs')).toEqual(localizedURLs);
41 | });
42 |
43 | test('defaultLanguage is passed in correctly', () => {
44 | expect(LangRouter.__get__('defaultLanguage')).toEqual(defaultLanguage);
45 | });
46 |
47 | test('i18nOptions are passed in correctly', () => {
48 | expect(LangRouter.__get__('i18n').dateTimeFormats).toEqual(dateTimeFormats);
49 | expect(LangRouter.__get__('i18n').pluralizationRules).toEqual(pluralizationRules);
50 | });
51 | });
52 |
53 |
54 | describe('LangRouter', () => {
55 |
56 | test('is an instance of VueRouter', () => {
57 | expect(router).toBeInstanceOf(VueRouter);
58 | });
59 |
60 | test('generates correct router aliases', () => {
61 | expect(routes[0].alias).toEqual([
62 | '/en/about-replaced',
63 | '/cs/o-nas',
64 | '/ru/about',
65 | ]);
66 | expect(routes[1].alias).toEqual([
67 | '/en/user/:slug',
68 | '/cs/uzivatel/:slug',
69 | '/ru/user/:slug',
70 | ]);
71 | expect(routes[1].children[0].alias).toEqual([
72 | 'detail',
73 | 'informatsiya',
74 | ]);
75 | });
76 |
77 | test('gets preffered language from browser', () => {
78 | Object.defineProperties(window.navigator, {
79 | browserLanguage: {
80 | value: 'ru',
81 | },
82 | language: {
83 | value: undefined,
84 | },
85 | languages: {
86 | value: null,
87 | configurable: true,
88 | },
89 | });
90 | expect(LangRouter.__get__('getPrefferedLanguage')()).toBe('ru');
91 |
92 | Object.defineProperty(window.navigator, 'languages', {
93 | value: [
94 | 'cs-CZ',
95 | 'en-US',
96 | ],
97 | configurable: true,
98 | });
99 | expect(LangRouter.__get__('getPrefferedLanguage')()).toBe('cs');
100 | });
101 |
102 | test('localizes path correctly', () => {
103 | const vueMock = {
104 | $router: router,
105 | };
106 | const localizePath = (path, lang) => {
107 | return LangRouter.__get__('localizePath').call(vueMock, path, lang);
108 | };
109 |
110 | expect(localizePath('/about', 'ru')).toBe('/ru/about');
111 | expect(localizePath('/cs/o-nas', 'en')).toBe('/en/about-replaced');
112 | expect(localizePath('/cs/o-nas', 'cs')).toBe('/cs/o-nas');
113 | expect(localizePath('/en/about-replaced', 'en')).toBe('/en/about-replaced');
114 |
115 | expect(localizePath('/user/2/info', 'ru')).toBe('/ru/user/2/informatsiya');
116 | expect(localizePath('/user/john-smith', 'en')).toBe('/user/john-smith');
117 | expect(localizePath('/user/john-smith', 'cs')).toBe('/cs/uzivatel/john-smith');
118 | expect(localizePath('/ru/user/6/informatsiya', 'cs')).toBe('/cs/uzivatel/6/detail');
119 |
120 | const originalConsoleError = console.error;
121 | console.error = jest.fn();
122 |
123 | expect(localizePath('/cs/undefined-path/detail', 'en')).toBe('/en/undefined-path/info');
124 | expect(localizePath('/cs/undefined-path/detail', 'ru')).toBe('/ru/undefined-path/informatsiya');
125 | expect(console.error).toHaveBeenCalledTimes(2);
126 |
127 | console.error = originalConsoleError;
128 |
129 | expect(localizePath('/ru/user/should-not-be-replaced/informatsiya', 'cs')).toBe('/cs/uzivatel/should-not-be-replaced/detail');
130 | });
131 |
132 | test('localizes path with query correctly', () => {
133 | const vueMock = {
134 | $router: router,
135 | };
136 | const localizePath = (path, lang) => {
137 | return LangRouter.__get__('localizePath').call(vueMock, path, lang);
138 | };
139 |
140 | expect(localizePath('/about/?q=test', 'ru')).toBe('/ru/about/?q=test');
141 | expect(localizePath('/cs/o-nas?q=test', 'en')).toBe('/en/about-replaced?q=test');
142 |
143 | expect(localizePath('/about/#hash', 'ru')).toBe('/ru/about/#hash');
144 | expect(localizePath('/cs/o-nas#hash', 'en')).toBe('/en/about-replaced#hash');
145 |
146 | expect(localizePath('/about/?q=test#hash', 'ru')).toBe('/ru/about/?q=test#hash');
147 | expect(localizePath('/cs/o-nas?q=test#hash', 'en')).toBe('/en/about-replaced?q=test#hash');
148 | });
149 | });
150 |
--------------------------------------------------------------------------------
/dist/lang-router.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-lang-router v1.3.1
3 | * (c) 2021 Radek Altof
4 | * Released under the MIT License.
5 | */
6 |
7 | var LangRouter=function(t,e,n){"use strict";function r(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var a=r(e),o=r(n);var i=Object.assign||function(){for(var t=arguments,e={},n=0;n Note: This example uses Czech and English translations.
67 |
68 | Example file structure:
69 |
70 | ```sh
71 | src
72 | └── lang
73 | └── translations
74 | ├── cs.json
75 | ├── en.json
76 | └── index.js
77 | ```
78 |
79 | Create `JSON` files for all desired languages and reference them in `index.js`. Translations are loaded on demand.
80 |
81 | ```javascript
82 | /* src/lang/translations/en.json */
83 |
84 | {
85 | "hello": "Hello!",
86 | "about": {
87 | "example": "This is an About page."
88 | }
89 | }
90 | ```
91 |
92 | ```javascript
93 | /* src/lang/translations/cs.json */
94 |
95 | {
96 | "hello": "Ahoj!",
97 | "about": {
98 | "example": "Tohle je stránka O nás."
99 | }
100 | }
101 | ```
102 |
103 | ```javascript
104 | /* src/lang/translations/index.js */
105 |
106 | export default {
107 | en: {
108 | name: 'English',
109 | load: () => { return import('./en.json'); },
110 | },
111 | cs: {
112 | name: 'Česky',
113 | load: () => { return import('./cs.json'); },
114 | },
115 | };
116 | ```
117 |
118 | In case you do not want to load translations asynchronously, you can provide `messages` object with translations instead of `load` function. For example:
119 |
120 | ```javascript
121 | export default {
122 | en: {
123 | name: 'English',
124 | messages: {
125 | hello: 'Hello!',
126 | },
127 | },
128 | };
129 | ```
130 |
131 | #### 3. Create localized URLs in your app.
132 |
133 | > Note: Localized URLs are optional.
134 |
135 | Example file structure:
136 |
137 | ```sh
138 | src
139 | └── lang
140 | └── localized-urls
141 | ├── cs.json
142 | └── index.js
143 | ```
144 |
145 | Create `JSON` files for all desired languages and `import` them in `index.js`. Localized URLs need to be imported before router instantiation.
146 |
147 | ```javascript
148 | /* src/lang/localized-urls/cs.json */
149 |
150 | {
151 | "about": "o-nas"
152 | }
153 | ```
154 |
155 | ```javascript
156 | /* src/lang/localized-urls/index.js */
157 |
158 | import cs from './cs.json';
159 |
160 | export default { cs };
161 | ```
162 |
163 | Language router will parse any given path and attempt to localize its segments based on this configuration. If no match is found, the original path segment is retained.
164 |
165 |
166 | #### 4. Modify your router file
167 |
168 | - Import `LangRouter` and use it instead of `VueRouter`.
169 | - Import translations and localized URLs and pass them to `LangRouter` plugin.
170 | - Create new router instance. All the options are the same as in `VueRouter`.
171 |
172 | ```javascript
173 | /* src/router/index.js */
174 |
175 | import Vue from 'vue';
176 | import LangRouter from 'vue-lang-router';
177 |
178 | import translations from '../lang/translations';
179 | import localizedURLs from '../lang/localized-urls';
180 |
181 | Vue.use(LangRouter, {
182 | defaultLanguage: 'en',
183 | translations,
184 | localizedURLs,
185 | });
186 |
187 | const routes = [ /* Your routes here */ ];
188 |
189 | const router = new LangRouter({
190 | routes,
191 | mode: 'history',
192 | });
193 | ```
194 |
195 | #### 5. Modify your main file
196 |
197 | Import `i18n` and use it in your `Vue` instance.
198 |
199 | ```javascript
200 | import Vue from 'vue';
201 | import App from './App.vue';
202 | import router from './router';
203 | import { i18n } from 'vue-lang-router';
204 |
205 | new Vue({
206 | router,
207 | i18n,
208 | render: h => h(App),
209 | }).$mount('#app');
210 | ```
211 |
212 |
213 | ## How to use
214 |
215 | ### Using translations
216 |
217 | To use any translated string, use `$t('stringName')` in your code. For more information check out [`Vue I18n`](http://kazupon.github.io/vue-i18n/).
218 |
219 |
220 | ### Using links
221 |
222 | Use `` component. It localizes given router path based on current language. It's a simple, yet powerful component, which takes a hassle out of generating proper links on your site.
223 |
224 | It accepts the same options as [``](https://router.vuejs.org/api/#router-link-props).
225 |
226 | ```html
227 | John Smith
228 | ```
229 |
230 | The above code will generate a link with `href` value depending on various factors. Here are a few examples:
231 |
232 | ```javascript
233 | /*
234 | Default language: "en"
235 | Current language: "en"
236 | Localized URLs "en": {}
237 | Current URL: /
238 | */
239 |
240 | href="/user/john-smith"
241 |
242 |
243 | /*
244 | Default language: "en"
245 | Current language: "en"
246 | Localized URLs "en": { "user": "u" }
247 | Current URL: /en/example
248 | */
249 |
250 | href="/en/u/john-smith"
251 |
252 |
253 | /*
254 | Default language: "en"
255 | Current language: "cs"
256 | Localized URLs "cs": { "user": "uzivatel" }
257 | Current URL: /
258 | */
259 |
260 | href="/cs/uzivatel/john-smith"
261 | ```
262 |
263 |
264 |
265 | ### Switching language
266 |
267 | Use `` component for this. The component will loop over all available languages and generate `links` array with properties, which you can use to create your own menu.
268 |
269 | The wrapper element will have `router-language-switcher` class.
270 |
271 | #### Examples:
272 |
273 | ```html
274 |
275 |
276 |
277 | {{ link.langName }}
278 |
279 |
280 | ```
281 |
282 | ```html
283 |
284 |
285 | {{ link.langIndex }}
286 |
287 |
288 | ```
289 |
290 | #### Properties:
291 |
292 | - **url** : The localized path to the current/specified page in iterated language.
293 | - **langIndex** : The index of the iterated language provided in `translations`.
294 | - **langName** : The name of the iterated language provided in `translations`.
295 | - **activeClass** : Returns the active class when iterated language equals current language, otherwise empty.
296 |
297 |
298 | #### Accepted attributes:
299 |
300 | - **tag** : Use this attribute to specify which tag should be used as a wrapper element. The default is `div`.
301 | - **url** : Provides a specific path to generate translation links for. If omitted, current path is used.
302 | - **active-class** : Defines the name of class to provide when language of the link equals current language. The default is `router-active-language`.
303 |
304 |
305 | ### Vue I18n customization
306 |
307 | In case you'd like to pass in custom `vue-i18n` options, you can do so using `i18nOptions` property, for example:
308 |
309 | ```javascript
310 | /* src/router/index.js */
311 |
312 | Vue.use(LangRouter, {
313 | defaultLanguage: 'en',
314 | translations,
315 | localizedURLs,
316 | i18nOptions: {
317 | dateTimeFormats: { ... },
318 | pluralizationRules: { ... },
319 | }
320 | });
321 | ```
322 |
--------------------------------------------------------------------------------
/src/plugin/index.js:
--------------------------------------------------------------------------------
1 | import VueI18n from 'vue-i18n';
2 | import VueRouter from 'vue-router';
3 |
4 | import LocalizedLink from './components/LocalizedLink.vue';
5 | import LanguageSwitcher from './components/LanguageSwitcher.vue';
6 |
7 |
8 | // Define vars
9 | let defaultLanguage, translations, localizedURLs, i18n;
10 |
11 |
12 | // Array of loaded translations
13 | const loadedTranslations = [];
14 |
15 |
16 | // Error logging
17 | function err (msg, error) {
18 | console.error('LangRouter: ' + msg);
19 | if (typeof error !== 'undefined') { console.error(error); }
20 | }
21 |
22 |
23 | // LangRouter class adds localized URL functionality to Vue Router
24 | export default class LangRouter {
25 |
26 | // Called when instantiated
27 | constructor (options) {
28 |
29 | // If any language is missing from localized URLs, add it as an empty object
30 | // All aliases need to be created for language switching purposes
31 | for (let lang in translations) {
32 | if (translations.hasOwnProperty(lang) && !localizedURLs[lang]) {
33 | localizedURLs[lang] = {};
34 | }
35 | }
36 |
37 | // Cycle through all the available languages and add aliases to routes
38 | for (let lang in localizedURLs) {
39 | if (localizedURLs.hasOwnProperty(lang)) {
40 | addAliasesToRoutes(options.routes, lang);
41 | }
42 | }
43 |
44 | // Create Vue Router instance
45 | const router = new VueRouter(options);
46 |
47 | // Add language switching logic
48 | router.beforeEach(switchLanguage);
49 |
50 | // Return Vue Router instance
51 | return router;
52 | }
53 | }
54 |
55 |
56 | // Install method of the LangRouter plugin
57 | LangRouter.install = function (Vue, options) {
58 |
59 | // Check if plugin is installed already
60 | if (LangRouter.installed) {
61 | err('Already installed.');
62 | return;
63 | }
64 | LangRouter.installed = true;
65 |
66 | // Get the options
67 | if (!options) {
68 | err('Options missing.');
69 | }
70 | defaultLanguage = options.defaultLanguage;
71 | translations = options.translations;
72 | localizedURLs = options.localizedURLs || {};
73 |
74 | // Check if variables look okay
75 | let isArr;
76 | if ((isArr = Array.isArray(translations)) || typeof translations !== 'object') {
77 | err('options.translations should be an object, received ' + (isArr ? 'array' : typeof translations) + ' instead.');
78 | }
79 | if ((isArr = Array.isArray(localizedURLs)) || typeof localizedURLs !== 'object') {
80 | err('options.localizedURLs should be an object, received ' + (isArr ? 'array' : typeof localizedURLs) + ' instead.');
81 | }
82 | if (typeof defaultLanguage !== 'string') {
83 | err('options.defaultLanguage should be a string, received ' + typeof defaultLanguage + ' instead.');
84 | }
85 |
86 | // Register plugins
87 | Vue.use(VueI18n);
88 | Vue.use(VueRouter);
89 |
90 | // Check if any translations are already present, if yes, pass them to VueI18n
91 | let messages = {};
92 |
93 | for (let lang in translations) {
94 | if (translations.hasOwnProperty(lang)) {
95 | let langMessages = translations[lang].messages;
96 |
97 | if (typeof langMessages === 'object' && !Array.isArray(langMessages)) {
98 | messages[lang] = translations[lang].messages;
99 | loadedTranslations.push(lang);
100 | }
101 | }
102 | }
103 |
104 | // Init internalization plugin
105 | i18n = new VueI18n({
106 | locale: defaultLanguage,
107 | fallbackLocale: defaultLanguage,
108 | messages,
109 | ...options.i18nOptions,
110 | });
111 |
112 | // Add translations to use in
113 | Vue.prototype._langRouter = { translations };
114 |
115 | // Add $localizePath method to return localized path
116 | Vue.prototype.$localizePath = localizePath;
117 |
118 | // Register components
119 | Vue.component('localized-link', LocalizedLink);
120 | Vue.component('language-switcher', LanguageSwitcher);
121 | };
122 |
123 |
124 | // Switching to a loaded language
125 | function setLanguage (lang) {
126 | i18n.locale = lang;
127 | document.querySelector('html').setAttribute('lang', lang);
128 | localStorage.setItem('VueAppLanguage', lang);
129 | return lang;
130 | }
131 |
132 |
133 | // Loading translations asynchronously
134 | // Returns promise
135 | function loadLanguage (lang) {
136 |
137 | // If the translation is already loaded
138 | if (loadedTranslations.includes(lang)) {
139 | return Promise.resolve(setLanguage(lang));
140 | }
141 |
142 | // If the translation hasn't been loaded
143 | // Check if the load function exists
144 | if (!translations[lang] || typeof translations[lang].load !== 'function') {
145 | err('Unable to load translations for "' + lang + '", "load" function is missing!');
146 | }
147 |
148 | // Load the translation
149 | return translations[lang].load().then(function (messages) {
150 | i18n.setLocaleMessage(lang, messages.default || messages);
151 | loadedTranslations.push(lang);
152 | return setLanguage(lang);
153 | }).catch(function (error) {
154 | err('Failed to load "' + lang + '" translation.', error);
155 | });
156 | }
157 |
158 |
159 | // Adding aliases to routes
160 | function addAliasesToRoutes (routes, lang, child) {
161 |
162 | // Iterate over each route
163 | routes.forEach(function (route) {
164 |
165 | // Translate the path
166 | let alias = translatePath(route.path, lang);
167 |
168 | // Add language prefix to alias (only if route is at the top level)
169 | if (!child) { alias = '/' + lang + (alias.charAt(0) != '/' ? '/' : '') + alias; }
170 |
171 | // Make sure alias array exists & add any pre-existing value to it
172 | if (route.alias) {
173 | if (!Array.isArray(route.alias)) { route.alias = [ route.alias ]; }
174 | }
175 | else { route.alias = []; }
176 |
177 | // Push alias into alias array
178 | if (route.path != alias && route.alias.indexOf(alias) == -1) { route.alias.push(alias); }
179 |
180 | // If the route has children, iterate over those too
181 | if (route.children) { addAliasesToRoutes(route.children, lang, true); }
182 |
183 | });
184 | }
185 |
186 |
187 | // Router - language switching
188 | function switchLanguage (to, from, next) {
189 | let lang = to.path.split('/')[1];
190 |
191 | // If language isn't available in the URL
192 | if (!translations[lang]) {
193 |
194 | // Set the language to saved one if available
195 | const savedLang = localStorage.getItem('VueAppLanguage');
196 | if (savedLang && translations[savedLang]) { lang = savedLang; }
197 | else {
198 |
199 | // Set the language to preferred one if available
200 | const preferredLang = getPrefferedLanguage();
201 | if (preferredLang && translations[preferredLang]) { lang = preferredLang; }
202 |
203 | // Otherwise set default language
204 | else { lang = defaultLanguage; }
205 | }
206 |
207 | // If the language isn't default one, translate path and redirect to it
208 | if (lang != defaultLanguage) {
209 |
210 | // Translate path
211 | let translatedPath = translatePath(to.path, lang);
212 |
213 | // Add language prefix to the path
214 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
215 |
216 | return next({ path: translatedPath, query: to.query, hash: to.hash });
217 | }
218 | }
219 |
220 | // Load requested language
221 | loadLanguage(lang).then(function () {
222 | return next();
223 | });
224 | }
225 |
226 |
227 | // Path translation
228 | function translatePath (path, langTo, langFrom, matchedPath) {
229 |
230 | // Split the path into chunks
231 | let pathChunks = path.split('/');
232 |
233 | // If the path is in some language already
234 | if (langFrom && localizedURLs[langFrom]) {
235 |
236 | // If the path language & the desired language are equal, do not translate
237 | if (langTo == langFrom) { return path; }
238 |
239 | // Create reversed map of localized URLs in given language
240 | const map = localizedURLs[langFrom];
241 | const reversedMap = {};
242 | Object.keys(map).forEach(function (key) {
243 | reversedMap[map[key]] = key;
244 | });
245 |
246 | // Split the matched path into chunks
247 | let matchedPathChunks = matchedPath.split('/');
248 |
249 | // Translate the path back to original path names
250 | for (let i = 0; i < pathChunks.length; i++) {
251 | let pathChunk = pathChunks[i];
252 |
253 | // If the original path chunk is a variable, do not translate it
254 | if (matchedPathChunks[i].charAt(0) == ':') { continue; }
255 |
256 | // If there is an alias, use it, otherwise use given path
257 | pathChunks[i] = reversedMap[pathChunk] || pathChunk;
258 | }
259 | }
260 |
261 | // Translate all the non-variable chunks of the path
262 | for (let i = 0; i < pathChunks.length; i++) {
263 | let pathChunk = pathChunks[i];
264 |
265 | // If the path chunk is a variable, do not translate it
266 | if (pathChunk.charAt(0) == ':') { continue; }
267 |
268 | // If there is an alias, use it, otherwise use given path
269 | pathChunks[i] = localizedURLs[langTo][pathChunk] || pathChunk;
270 | }
271 |
272 | // Join path chunks and return
273 | return pathChunks.join('/');
274 | }
275 |
276 |
277 | // Retrieving preferred language from browser
278 | function getPrefferedLanguage () {
279 |
280 | // Extraction of language shortcut from language string
281 | function extractLanguage (s) {
282 | return s.split('-')[0].toLowerCase();
283 | }
284 |
285 | // Use navigator.languages if available
286 | if (navigator.languages && navigator.languages.length) { return extractLanguage(navigator.languages[0] || ''); }
287 |
288 | // Otherwise use whatever is available
289 | return extractLanguage(navigator.language || navigator.browserLanguage || navigator.userLanguage || '');
290 | }
291 |
292 |
293 | // Path localization
294 | function localizePath (fullPath, lang) {
295 | // If the desired language is not defined or it doesn't exist, use current one
296 | if (!lang || !localizedURLs[lang]) { lang = i18n.locale; }
297 |
298 | // Separate path & query
299 | let path = fullPath;
300 | let query = '';
301 |
302 | if (fullPath.includes('?')) {
303 | path = fullPath.split('?')[0];
304 | query = '?' + fullPath.split('?')[1];
305 | }
306 | else if (fullPath.includes('#')) {
307 | path = fullPath.split('#')[0];
308 | query = '#' + fullPath.split('#')[1];
309 | }
310 |
311 | // Split path into chunks
312 | const pathChunks = path.split('/');
313 |
314 | // Get the path language, if there is any
315 | let pathLang = (localizedURLs[pathChunks[1]] ? pathChunks[1] : false);
316 |
317 | // If the language is default language
318 | // & current path doesn't contain a language
319 | // & path to translate doesn't contain a language
320 | // = no need to localize
321 | const currentPathLang = this.$router.currentRoute.path.split('/')[1];
322 | if (lang == defaultLanguage && !localizedURLs[currentPathLang] && !pathLang) { return fullPath; }
323 |
324 | // If the path is in some language already
325 | let resolvedPath = false;
326 | if (pathLang) {
327 | // Get the original path
328 | const resolvedRoute = this.$router.resolve(path);
329 |
330 | if (resolvedRoute.route.matched.length != 0) {
331 | resolvedPath = resolvedRoute.route.matched[resolvedRoute.route.matched.length - 1].path;
332 | resolvedPath = (resolvedPath.charAt(0) == '/' ? resolvedPath : '/' + resolvedPath);
333 | }
334 | else {
335 | err('Router could not resolve path "' + path + '". URL localization may not work as expected.');
336 | }
337 |
338 | // Remove the language from path
339 | pathChunks.splice(1, 1);
340 | path = pathChunks.join('/');
341 | }
342 |
343 | // Translate path
344 | let translatedPath = translatePath(path, lang, pathLang, (resolvedPath || path));
345 |
346 | // Add language prefix to the path
347 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
348 |
349 | return translatedPath + query;
350 | }
351 |
352 |
353 | // Automatic plugin installation if in browser
354 | if (typeof window !== 'undefined' && window.Vue) {
355 | window.Vue.use(LangRouter);
356 | }
357 |
358 |
359 | // Export what's needed
360 | export { i18n };
361 |
--------------------------------------------------------------------------------
/dist/lang-router.esm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-lang-router v1.3.1
3 | * (c) 2021 Radek Altof
4 | * Released under the MIT License.
5 | */
6 |
7 | import VueI18n from 'vue-i18n';
8 | import VueRouter from 'vue-router';
9 |
10 | function mergeObjects() {
11 | var arguments$1 = arguments;
12 | var targetObject = {};
13 | for (var i = 0; i < arguments.length; i++) {
14 | var sourceObject = arguments$1[i];
15 | for (var key in sourceObject) {
16 | if (Object.prototype.hasOwnProperty.call(sourceObject, key)) {
17 | targetObject[key] = sourceObject[key];
18 | }
19 | }
20 | }
21 | return targetObject;
22 | }
23 | var _spread = Object.assign || mergeObjects;
24 |
25 | var script$1 = {
26 | name: 'LocalizedLink',
27 | props: [ 'to' ],
28 | methods: {
29 | localizedTo: function localizedTo () {
30 | if (typeof this.to === 'string') {
31 | return this.$localizePath(this.to);
32 | }
33 | else if (typeof this.to === 'object' && typeof this.to.path === 'string') {
34 | var o = JSON.parse(JSON.stringify(this.to));
35 | o.path = this.$localizePath(o.path);
36 | return o;
37 | }
38 | else {
39 | return this.to;
40 | }
41 | },
42 | },
43 | };
44 |
45 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
46 | if (typeof shadowMode !== 'boolean') {
47 | createInjectorSSR = createInjector;
48 | createInjector = shadowMode;
49 | shadowMode = false;
50 | }
51 | var options = typeof script === 'function' ? script.options : script;
52 | if (template && template.render) {
53 | options.render = template.render;
54 | options.staticRenderFns = template.staticRenderFns;
55 | options._compiled = true;
56 | if (isFunctionalTemplate) {
57 | options.functional = true;
58 | }
59 | }
60 | if (scopeId) {
61 | options._scopeId = scopeId;
62 | }
63 | var hook;
64 | if (moduleIdentifier) {
65 | hook = function (context) {
66 | context =
67 | context ||
68 | (this.$vnode && this.$vnode.ssrContext) ||
69 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext);
70 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
71 | context = __VUE_SSR_CONTEXT__;
72 | }
73 | if (style) {
74 | style.call(this, createInjectorSSR(context));
75 | }
76 | if (context && context._registeredComponents) {
77 | context._registeredComponents.add(moduleIdentifier);
78 | }
79 | };
80 | options._ssrRegister = hook;
81 | }
82 | else if (style) {
83 | hook = shadowMode
84 | ? function (context) {
85 | style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
86 | }
87 | : function (context) {
88 | style.call(this, createInjector(context));
89 | };
90 | }
91 | if (hook) {
92 | if (options.functional) {
93 | var originalRender = options.render;
94 | options.render = function renderWithStyleInjection(h, context) {
95 | hook.call(context);
96 | return originalRender(h, context);
97 | };
98 | }
99 | else {
100 | var existing = options.beforeCreate;
101 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
102 | }
103 | }
104 | return script;
105 | }
106 |
107 | /* script */
108 | var __vue_script__$1 = script$1;
109 |
110 | /* template */
111 | var __vue_render__$1 = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('router-link',_vm._b({attrs:{"to":_vm.localizedTo()}},'router-link',_vm.$attrs,false),[_vm._t("default")],2)};
112 | var __vue_staticRenderFns__$1 = [];
113 |
114 | /* style */
115 | var __vue_inject_styles__$1 = undefined;
116 | /* scoped */
117 | var __vue_scope_id__$1 = undefined;
118 | /* module identifier */
119 | var __vue_module_identifier__$1 = undefined;
120 | /* functional template */
121 | var __vue_is_functional_template__$1 = false;
122 | /* style inject */
123 |
124 | /* style inject SSR */
125 |
126 | /* style inject shadow dom */
127 |
128 |
129 |
130 | var __vue_component__$1 = /*#__PURE__*/normalizeComponent(
131 | { render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
132 | __vue_inject_styles__$1,
133 | __vue_script__$1,
134 | __vue_scope_id__$1,
135 | __vue_is_functional_template__$1,
136 | __vue_module_identifier__$1,
137 | false,
138 | undefined,
139 | undefined,
140 | undefined
141 | );
142 |
143 | var script = {
144 | name: 'LanguageSwitcher',
145 | data: function data () {
146 | return {
147 | currentUrl: this.url || this.$router.currentRoute.fullPath,
148 | links: [],
149 | };
150 | },
151 | props: [ 'tag', 'active-class', 'url' ],
152 | methods: {
153 | getTag: function getTag () {
154 | if (this.tag) { return this.tag; }
155 | else { return 'div'; }
156 | },
157 | generateLinks: function generateLinks () {
158 | var links = [];
159 | var activeClass = this.activeClass || 'router-active-language';
160 | var tr = this._langRouter.translations;
161 | for (var lang in tr) {
162 | if (tr.hasOwnProperty(lang)) {
163 | links.push({
164 | activeClass: (lang == this.$i18n.locale ? activeClass : ''),
165 | langIndex: lang,
166 | langName: tr[lang].name || lang,
167 | url: this.$localizePath(this.currentUrl, lang),
168 | });
169 | }
170 | }
171 | this.links = links;
172 | },
173 | },
174 | watch: {
175 | $route: function $route (to) {
176 | this.currentUrl = this.url || to.fullPath;
177 | this.generateLinks();
178 | },
179 | },
180 | mounted: function mounted () {
181 | this.generateLinks();
182 | },
183 | };
184 |
185 | /* script */
186 | var __vue_script__ = script;
187 |
188 | /* template */
189 | var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c(_vm.getTag(),{tag:"component",staticClass:"router-language-switcher"},[_vm._t("default",null,{"links":_vm.links})],2)};
190 | var __vue_staticRenderFns__ = [];
191 |
192 | /* style */
193 | var __vue_inject_styles__ = undefined;
194 | /* scoped */
195 | var __vue_scope_id__ = undefined;
196 | /* module identifier */
197 | var __vue_module_identifier__ = undefined;
198 | /* functional template */
199 | var __vue_is_functional_template__ = false;
200 | /* style inject */
201 |
202 | /* style inject SSR */
203 |
204 | /* style inject shadow dom */
205 |
206 |
207 |
208 | var __vue_component__ = /*#__PURE__*/normalizeComponent(
209 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
210 | __vue_inject_styles__,
211 | __vue_script__,
212 | __vue_scope_id__,
213 | __vue_is_functional_template__,
214 | __vue_module_identifier__,
215 | false,
216 | undefined,
217 | undefined,
218 | undefined
219 | );
220 |
221 | var defaultLanguage, translations, localizedURLs, i18n;
222 | var loadedTranslations = [];
223 | function err (msg, error) {
224 | console.error('LangRouter: ' + msg);
225 | if (typeof error !== 'undefined') { console.error(error); }
226 | }
227 | var LangRouter = function LangRouter (options) {
228 | for (var lang in translations) {
229 | if (translations.hasOwnProperty(lang) && !localizedURLs[lang]) {
230 | localizedURLs[lang] = {};
231 | }
232 | }
233 | for (var lang$1 in localizedURLs) {
234 | if (localizedURLs.hasOwnProperty(lang$1)) {
235 | addAliasesToRoutes(options.routes, lang$1);
236 | }
237 | }
238 | var router = new VueRouter(options);
239 | router.beforeEach(switchLanguage);
240 | return router;
241 | };
242 | LangRouter.install = function (Vue, options) {
243 | if (LangRouter.installed) {
244 | err('Already installed.');
245 | return;
246 | }
247 | LangRouter.installed = true;
248 | if (!options) {
249 | err('Options missing.');
250 | }
251 | defaultLanguage = options.defaultLanguage;
252 | translations = options.translations;
253 | localizedURLs = options.localizedURLs || {};
254 | var isArr;
255 | if ((isArr = Array.isArray(translations)) || typeof translations !== 'object') {
256 | err('options.translations should be an object, received ' + (isArr ? 'array' : typeof translations) + ' instead.');
257 | }
258 | if ((isArr = Array.isArray(localizedURLs)) || typeof localizedURLs !== 'object') {
259 | err('options.localizedURLs should be an object, received ' + (isArr ? 'array' : typeof localizedURLs) + ' instead.');
260 | }
261 | if (typeof defaultLanguage !== 'string') {
262 | err('options.defaultLanguage should be a string, received ' + typeof defaultLanguage + ' instead.');
263 | }
264 | Vue.use(VueI18n);
265 | Vue.use(VueRouter);
266 | var messages = {};
267 | for (var lang in translations) {
268 | if (translations.hasOwnProperty(lang)) {
269 | var langMessages = translations[lang].messages;
270 | if (typeof langMessages === 'object' && !Array.isArray(langMessages)) {
271 | messages[lang] = translations[lang].messages;
272 | loadedTranslations.push(lang);
273 | }
274 | }
275 | }
276 | i18n = new VueI18n(_spread({}, {locale: defaultLanguage,
277 | fallbackLocale: defaultLanguage,
278 | messages: messages},
279 | options.i18nOptions));
280 | Vue.prototype._langRouter = { translations: translations };
281 | Vue.prototype.$localizePath = localizePath;
282 | Vue.component('localized-link', __vue_component__$1);
283 | Vue.component('language-switcher', __vue_component__);
284 | };
285 | function setLanguage (lang) {
286 | i18n.locale = lang;
287 | document.querySelector('html').setAttribute('lang', lang);
288 | localStorage.setItem('VueAppLanguage', lang);
289 | return lang;
290 | }
291 | function loadLanguage (lang) {
292 | if (loadedTranslations.includes(lang)) {
293 | return Promise.resolve(setLanguage(lang));
294 | }
295 | if (!translations[lang] || typeof translations[lang].load !== 'function') {
296 | err('Unable to load translations for "' + lang + '", "load" function is missing!');
297 | }
298 | return translations[lang].load().then(function (messages) {
299 | i18n.setLocaleMessage(lang, messages.default || messages);
300 | loadedTranslations.push(lang);
301 | return setLanguage(lang);
302 | }).catch(function (error) {
303 | err('Failed to load "' + lang + '" translation.', error);
304 | });
305 | }
306 | function addAliasesToRoutes (routes, lang, child) {
307 | routes.forEach(function (route) {
308 | var alias = translatePath(route.path, lang);
309 | if (!child) { alias = '/' + lang + (alias.charAt(0) != '/' ? '/' : '') + alias; }
310 | if (route.alias) {
311 | if (!Array.isArray(route.alias)) { route.alias = [ route.alias ]; }
312 | }
313 | else { route.alias = []; }
314 | if (route.path != alias && route.alias.indexOf(alias) == -1) { route.alias.push(alias); }
315 | if (route.children) { addAliasesToRoutes(route.children, lang, true); }
316 | });
317 | }
318 | function switchLanguage (to, from, next) {
319 | var lang = to.path.split('/')[1];
320 | if (!translations[lang]) {
321 | var savedLang = localStorage.getItem('VueAppLanguage');
322 | if (savedLang && translations[savedLang]) { lang = savedLang; }
323 | else {
324 | var preferredLang = getPrefferedLanguage();
325 | if (preferredLang && translations[preferredLang]) { lang = preferredLang; }
326 | else { lang = defaultLanguage; }
327 | }
328 | if (lang != defaultLanguage) {
329 | var translatedPath = translatePath(to.path, lang);
330 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
331 | return next({ path: translatedPath, query: to.query, hash: to.hash });
332 | }
333 | }
334 | loadLanguage(lang).then(function () {
335 | return next();
336 | });
337 | }
338 | function translatePath (path, langTo, langFrom, matchedPath) {
339 | var pathChunks = path.split('/');
340 | if (langFrom && localizedURLs[langFrom]) {
341 | if (langTo == langFrom) { return path; }
342 | var map = localizedURLs[langFrom];
343 | var reversedMap = {};
344 | Object.keys(map).forEach(function (key) {
345 | reversedMap[map[key]] = key;
346 | });
347 | var matchedPathChunks = matchedPath.split('/');
348 | for (var i = 0; i < pathChunks.length; i++) {
349 | var pathChunk = pathChunks[i];
350 | if (matchedPathChunks[i].charAt(0) == ':') { continue; }
351 | pathChunks[i] = reversedMap[pathChunk] || pathChunk;
352 | }
353 | }
354 | for (var i$1 = 0; i$1 < pathChunks.length; i$1++) {
355 | var pathChunk$1 = pathChunks[i$1];
356 | if (pathChunk$1.charAt(0) == ':') { continue; }
357 | pathChunks[i$1] = localizedURLs[langTo][pathChunk$1] || pathChunk$1;
358 | }
359 | return pathChunks.join('/');
360 | }
361 | function getPrefferedLanguage () {
362 | function extractLanguage (s) {
363 | return s.split('-')[0].toLowerCase();
364 | }
365 | if (navigator.languages && navigator.languages.length) { return extractLanguage(navigator.languages[0] || ''); }
366 | return extractLanguage(navigator.language || navigator.browserLanguage || navigator.userLanguage || '');
367 | }
368 | function localizePath (fullPath, lang) {
369 | if (!lang || !localizedURLs[lang]) { lang = i18n.locale; }
370 | var path = fullPath;
371 | var query = '';
372 | if (fullPath.includes('?')) {
373 | path = fullPath.split('?')[0];
374 | query = '?' + fullPath.split('?')[1];
375 | }
376 | else if (fullPath.includes('#')) {
377 | path = fullPath.split('#')[0];
378 | query = '#' + fullPath.split('#')[1];
379 | }
380 | var pathChunks = path.split('/');
381 | var pathLang = (localizedURLs[pathChunks[1]] ? pathChunks[1] : false);
382 | var currentPathLang = this.$router.currentRoute.path.split('/')[1];
383 | if (lang == defaultLanguage && !localizedURLs[currentPathLang] && !pathLang) { return fullPath; }
384 | var resolvedPath = false;
385 | if (pathLang) {
386 | var resolvedRoute = this.$router.resolve(path);
387 | if (resolvedRoute.route.matched.length != 0) {
388 | resolvedPath = resolvedRoute.route.matched[resolvedRoute.route.matched.length - 1].path;
389 | resolvedPath = (resolvedPath.charAt(0) == '/' ? resolvedPath : '/' + resolvedPath);
390 | }
391 | else {
392 | err('Router could not resolve path "' + path + '". URL localization may not work as expected.');
393 | }
394 | pathChunks.splice(1, 1);
395 | path = pathChunks.join('/');
396 | }
397 | var translatedPath = translatePath(path, lang, pathLang, (resolvedPath || path));
398 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
399 | return translatedPath + query;
400 | }
401 | if (typeof window !== 'undefined' && window.Vue) {
402 | window.Vue.use(LangRouter);
403 | }
404 |
405 | export { LangRouter as default, i18n };
406 |
--------------------------------------------------------------------------------
/dist/lang-router.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-lang-router v1.3.1
3 | * (c) 2021 Radek Altof
4 | * Released under the MIT License.
5 | */
6 |
7 | var LangRouter = (function (exports, VueI18n, VueRouter) {
8 | 'use strict';
9 |
10 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11 |
12 | var VueI18n__default = /*#__PURE__*/_interopDefaultLegacy(VueI18n);
13 | var VueRouter__default = /*#__PURE__*/_interopDefaultLegacy(VueRouter);
14 |
15 | function mergeObjects() {
16 | var arguments$1 = arguments;
17 | var targetObject = {};
18 | for (var i = 0; i < arguments.length; i++) {
19 | var sourceObject = arguments$1[i];
20 | for (var key in sourceObject) {
21 | if (Object.prototype.hasOwnProperty.call(sourceObject, key)) {
22 | targetObject[key] = sourceObject[key];
23 | }
24 | }
25 | }
26 | return targetObject;
27 | }
28 | var _spread = Object.assign || mergeObjects;
29 |
30 | var script$1 = {
31 | name: 'LocalizedLink',
32 | props: [ 'to' ],
33 | methods: {
34 | localizedTo: function localizedTo () {
35 | if (typeof this.to === 'string') {
36 | return this.$localizePath(this.to);
37 | }
38 | else if (typeof this.to === 'object' && typeof this.to.path === 'string') {
39 | var o = JSON.parse(JSON.stringify(this.to));
40 | o.path = this.$localizePath(o.path);
41 | return o;
42 | }
43 | else {
44 | return this.to;
45 | }
46 | },
47 | },
48 | };
49 |
50 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
51 | if (typeof shadowMode !== 'boolean') {
52 | createInjectorSSR = createInjector;
53 | createInjector = shadowMode;
54 | shadowMode = false;
55 | }
56 | var options = typeof script === 'function' ? script.options : script;
57 | if (template && template.render) {
58 | options.render = template.render;
59 | options.staticRenderFns = template.staticRenderFns;
60 | options._compiled = true;
61 | if (isFunctionalTemplate) {
62 | options.functional = true;
63 | }
64 | }
65 | if (scopeId) {
66 | options._scopeId = scopeId;
67 | }
68 | var hook;
69 | if (moduleIdentifier) {
70 | hook = function (context) {
71 | context =
72 | context ||
73 | (this.$vnode && this.$vnode.ssrContext) ||
74 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext);
75 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
76 | context = __VUE_SSR_CONTEXT__;
77 | }
78 | if (style) {
79 | style.call(this, createInjectorSSR(context));
80 | }
81 | if (context && context._registeredComponents) {
82 | context._registeredComponents.add(moduleIdentifier);
83 | }
84 | };
85 | options._ssrRegister = hook;
86 | }
87 | else if (style) {
88 | hook = shadowMode
89 | ? function (context) {
90 | style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
91 | }
92 | : function (context) {
93 | style.call(this, createInjector(context));
94 | };
95 | }
96 | if (hook) {
97 | if (options.functional) {
98 | var originalRender = options.render;
99 | options.render = function renderWithStyleInjection(h, context) {
100 | hook.call(context);
101 | return originalRender(h, context);
102 | };
103 | }
104 | else {
105 | var existing = options.beforeCreate;
106 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
107 | }
108 | }
109 | return script;
110 | }
111 |
112 | /* script */
113 | var __vue_script__$1 = script$1;
114 |
115 | /* template */
116 | var __vue_render__$1 = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('router-link',_vm._b({attrs:{"to":_vm.localizedTo()}},'router-link',_vm.$attrs,false),[_vm._t("default")],2)};
117 | var __vue_staticRenderFns__$1 = [];
118 |
119 | /* style */
120 | var __vue_inject_styles__$1 = undefined;
121 | /* scoped */
122 | var __vue_scope_id__$1 = undefined;
123 | /* module identifier */
124 | var __vue_module_identifier__$1 = undefined;
125 | /* functional template */
126 | var __vue_is_functional_template__$1 = false;
127 | /* style inject */
128 |
129 | /* style inject SSR */
130 |
131 | /* style inject shadow dom */
132 |
133 |
134 |
135 | var __vue_component__$1 = /*#__PURE__*/normalizeComponent(
136 | { render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
137 | __vue_inject_styles__$1,
138 | __vue_script__$1,
139 | __vue_scope_id__$1,
140 | __vue_is_functional_template__$1,
141 | __vue_module_identifier__$1,
142 | false,
143 | undefined,
144 | undefined,
145 | undefined
146 | );
147 |
148 | var script = {
149 | name: 'LanguageSwitcher',
150 | data: function data () {
151 | return {
152 | currentUrl: this.url || this.$router.currentRoute.fullPath,
153 | links: [],
154 | };
155 | },
156 | props: [ 'tag', 'active-class', 'url' ],
157 | methods: {
158 | getTag: function getTag () {
159 | if (this.tag) { return this.tag; }
160 | else { return 'div'; }
161 | },
162 | generateLinks: function generateLinks () {
163 | var links = [];
164 | var activeClass = this.activeClass || 'router-active-language';
165 | var tr = this._langRouter.translations;
166 | for (var lang in tr) {
167 | if (tr.hasOwnProperty(lang)) {
168 | links.push({
169 | activeClass: (lang == this.$i18n.locale ? activeClass : ''),
170 | langIndex: lang,
171 | langName: tr[lang].name || lang,
172 | url: this.$localizePath(this.currentUrl, lang),
173 | });
174 | }
175 | }
176 | this.links = links;
177 | },
178 | },
179 | watch: {
180 | $route: function $route (to) {
181 | this.currentUrl = this.url || to.fullPath;
182 | this.generateLinks();
183 | },
184 | },
185 | mounted: function mounted () {
186 | this.generateLinks();
187 | },
188 | };
189 |
190 | /* script */
191 | var __vue_script__ = script;
192 |
193 | /* template */
194 | var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c(_vm.getTag(),{tag:"component",staticClass:"router-language-switcher"},[_vm._t("default",null,{"links":_vm.links})],2)};
195 | var __vue_staticRenderFns__ = [];
196 |
197 | /* style */
198 | var __vue_inject_styles__ = undefined;
199 | /* scoped */
200 | var __vue_scope_id__ = undefined;
201 | /* module identifier */
202 | var __vue_module_identifier__ = undefined;
203 | /* functional template */
204 | var __vue_is_functional_template__ = false;
205 | /* style inject */
206 |
207 | /* style inject SSR */
208 |
209 | /* style inject shadow dom */
210 |
211 |
212 |
213 | var __vue_component__ = /*#__PURE__*/normalizeComponent(
214 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
215 | __vue_inject_styles__,
216 | __vue_script__,
217 | __vue_scope_id__,
218 | __vue_is_functional_template__,
219 | __vue_module_identifier__,
220 | false,
221 | undefined,
222 | undefined,
223 | undefined
224 | );
225 |
226 | var defaultLanguage, translations, localizedURLs; exports.i18n = void 0;
227 | var loadedTranslations = [];
228 | function err (msg, error) {
229 | console.error('LangRouter: ' + msg);
230 | if (typeof error !== 'undefined') { console.error(error); }
231 | }
232 | var LangRouter = function LangRouter (options) {
233 | for (var lang in translations) {
234 | if (translations.hasOwnProperty(lang) && !localizedURLs[lang]) {
235 | localizedURLs[lang] = {};
236 | }
237 | }
238 | for (var lang$1 in localizedURLs) {
239 | if (localizedURLs.hasOwnProperty(lang$1)) {
240 | addAliasesToRoutes(options.routes, lang$1);
241 | }
242 | }
243 | var router = new VueRouter__default['default'](options);
244 | router.beforeEach(switchLanguage);
245 | return router;
246 | };
247 | LangRouter.install = function (Vue, options) {
248 | if (LangRouter.installed) {
249 | err('Already installed.');
250 | return;
251 | }
252 | LangRouter.installed = true;
253 | if (!options) {
254 | err('Options missing.');
255 | }
256 | defaultLanguage = options.defaultLanguage;
257 | translations = options.translations;
258 | localizedURLs = options.localizedURLs || {};
259 | var isArr;
260 | if ((isArr = Array.isArray(translations)) || typeof translations !== 'object') {
261 | err('options.translations should be an object, received ' + (isArr ? 'array' : typeof translations) + ' instead.');
262 | }
263 | if ((isArr = Array.isArray(localizedURLs)) || typeof localizedURLs !== 'object') {
264 | err('options.localizedURLs should be an object, received ' + (isArr ? 'array' : typeof localizedURLs) + ' instead.');
265 | }
266 | if (typeof defaultLanguage !== 'string') {
267 | err('options.defaultLanguage should be a string, received ' + typeof defaultLanguage + ' instead.');
268 | }
269 | Vue.use(VueI18n__default['default']);
270 | Vue.use(VueRouter__default['default']);
271 | var messages = {};
272 | for (var lang in translations) {
273 | if (translations.hasOwnProperty(lang)) {
274 | var langMessages = translations[lang].messages;
275 | if (typeof langMessages === 'object' && !Array.isArray(langMessages)) {
276 | messages[lang] = translations[lang].messages;
277 | loadedTranslations.push(lang);
278 | }
279 | }
280 | }
281 | exports.i18n = new VueI18n__default['default'](_spread({}, {locale: defaultLanguage,
282 | fallbackLocale: defaultLanguage,
283 | messages: messages},
284 | options.i18nOptions));
285 | Vue.prototype._langRouter = { translations: translations };
286 | Vue.prototype.$localizePath = localizePath;
287 | Vue.component('localized-link', __vue_component__$1);
288 | Vue.component('language-switcher', __vue_component__);
289 | };
290 | function setLanguage (lang) {
291 | exports.i18n.locale = lang;
292 | document.querySelector('html').setAttribute('lang', lang);
293 | localStorage.setItem('VueAppLanguage', lang);
294 | return lang;
295 | }
296 | function loadLanguage (lang) {
297 | if (loadedTranslations.includes(lang)) {
298 | return Promise.resolve(setLanguage(lang));
299 | }
300 | if (!translations[lang] || typeof translations[lang].load !== 'function') {
301 | err('Unable to load translations for "' + lang + '", "load" function is missing!');
302 | }
303 | return translations[lang].load().then(function (messages) {
304 | exports.i18n.setLocaleMessage(lang, messages.default || messages);
305 | loadedTranslations.push(lang);
306 | return setLanguage(lang);
307 | }).catch(function (error) {
308 | err('Failed to load "' + lang + '" translation.', error);
309 | });
310 | }
311 | function addAliasesToRoutes (routes, lang, child) {
312 | routes.forEach(function (route) {
313 | var alias = translatePath(route.path, lang);
314 | if (!child) { alias = '/' + lang + (alias.charAt(0) != '/' ? '/' : '') + alias; }
315 | if (route.alias) {
316 | if (!Array.isArray(route.alias)) { route.alias = [ route.alias ]; }
317 | }
318 | else { route.alias = []; }
319 | if (route.path != alias && route.alias.indexOf(alias) == -1) { route.alias.push(alias); }
320 | if (route.children) { addAliasesToRoutes(route.children, lang, true); }
321 | });
322 | }
323 | function switchLanguage (to, from, next) {
324 | var lang = to.path.split('/')[1];
325 | if (!translations[lang]) {
326 | var savedLang = localStorage.getItem('VueAppLanguage');
327 | if (savedLang && translations[savedLang]) { lang = savedLang; }
328 | else {
329 | var preferredLang = getPrefferedLanguage();
330 | if (preferredLang && translations[preferredLang]) { lang = preferredLang; }
331 | else { lang = defaultLanguage; }
332 | }
333 | if (lang != defaultLanguage) {
334 | var translatedPath = translatePath(to.path, lang);
335 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
336 | return next({ path: translatedPath, query: to.query, hash: to.hash });
337 | }
338 | }
339 | loadLanguage(lang).then(function () {
340 | return next();
341 | });
342 | }
343 | function translatePath (path, langTo, langFrom, matchedPath) {
344 | var pathChunks = path.split('/');
345 | if (langFrom && localizedURLs[langFrom]) {
346 | if (langTo == langFrom) { return path; }
347 | var map = localizedURLs[langFrom];
348 | var reversedMap = {};
349 | Object.keys(map).forEach(function (key) {
350 | reversedMap[map[key]] = key;
351 | });
352 | var matchedPathChunks = matchedPath.split('/');
353 | for (var i = 0; i < pathChunks.length; i++) {
354 | var pathChunk = pathChunks[i];
355 | if (matchedPathChunks[i].charAt(0) == ':') { continue; }
356 | pathChunks[i] = reversedMap[pathChunk] || pathChunk;
357 | }
358 | }
359 | for (var i$1 = 0; i$1 < pathChunks.length; i$1++) {
360 | var pathChunk$1 = pathChunks[i$1];
361 | if (pathChunk$1.charAt(0) == ':') { continue; }
362 | pathChunks[i$1] = localizedURLs[langTo][pathChunk$1] || pathChunk$1;
363 | }
364 | return pathChunks.join('/');
365 | }
366 | function getPrefferedLanguage () {
367 | function extractLanguage (s) {
368 | return s.split('-')[0].toLowerCase();
369 | }
370 | if (navigator.languages && navigator.languages.length) { return extractLanguage(navigator.languages[0] || ''); }
371 | return extractLanguage(navigator.language || navigator.browserLanguage || navigator.userLanguage || '');
372 | }
373 | function localizePath (fullPath, lang) {
374 | if (!lang || !localizedURLs[lang]) { lang = exports.i18n.locale; }
375 | var path = fullPath;
376 | var query = '';
377 | if (fullPath.includes('?')) {
378 | path = fullPath.split('?')[0];
379 | query = '?' + fullPath.split('?')[1];
380 | }
381 | else if (fullPath.includes('#')) {
382 | path = fullPath.split('#')[0];
383 | query = '#' + fullPath.split('#')[1];
384 | }
385 | var pathChunks = path.split('/');
386 | var pathLang = (localizedURLs[pathChunks[1]] ? pathChunks[1] : false);
387 | var currentPathLang = this.$router.currentRoute.path.split('/')[1];
388 | if (lang == defaultLanguage && !localizedURLs[currentPathLang] && !pathLang) { return fullPath; }
389 | var resolvedPath = false;
390 | if (pathLang) {
391 | var resolvedRoute = this.$router.resolve(path);
392 | if (resolvedRoute.route.matched.length != 0) {
393 | resolvedPath = resolvedRoute.route.matched[resolvedRoute.route.matched.length - 1].path;
394 | resolvedPath = (resolvedPath.charAt(0) == '/' ? resolvedPath : '/' + resolvedPath);
395 | }
396 | else {
397 | err('Router could not resolve path "' + path + '". URL localization may not work as expected.');
398 | }
399 | pathChunks.splice(1, 1);
400 | path = pathChunks.join('/');
401 | }
402 | var translatedPath = translatePath(path, lang, pathLang, (resolvedPath || path));
403 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
404 | return translatedPath + query;
405 | }
406 | if (typeof window !== 'undefined' && window.Vue) {
407 | window.Vue.use(LangRouter);
408 | }
409 |
410 | exports['default'] = LangRouter;
411 |
412 | Object.defineProperty(exports, '__esModule', { value: true });
413 |
414 | return exports;
415 |
416 | }({}, VueI18n, VueRouter));
417 |
--------------------------------------------------------------------------------
/dist/lang-router.umd.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-lang-router v1.3.1
3 | * (c) 2021 Radek Altof
4 | * Released under the MIT License.
5 | */
6 |
7 | (function (global, factory) {
8 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue-i18n'), require('vue-router')) :
9 | typeof define === 'function' && define.amd ? define(['exports', 'vue-i18n', 'vue-router'], factory) :
10 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LangRouter = {}, global.VueI18n, global.VueRouter));
11 | }(this, (function (exports, VueI18n, VueRouter) { 'use strict';
12 |
13 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
14 |
15 | var VueI18n__default = /*#__PURE__*/_interopDefaultLegacy(VueI18n);
16 | var VueRouter__default = /*#__PURE__*/_interopDefaultLegacy(VueRouter);
17 |
18 | function mergeObjects() {
19 | var arguments$1 = arguments;
20 | var targetObject = {};
21 | for (var i = 0; i < arguments.length; i++) {
22 | var sourceObject = arguments$1[i];
23 | for (var key in sourceObject) {
24 | if (Object.prototype.hasOwnProperty.call(sourceObject, key)) {
25 | targetObject[key] = sourceObject[key];
26 | }
27 | }
28 | }
29 | return targetObject;
30 | }
31 | var _spread = Object.assign || mergeObjects;
32 |
33 | var script$1 = {
34 | name: 'LocalizedLink',
35 | props: [ 'to' ],
36 | methods: {
37 | localizedTo: function localizedTo () {
38 | if (typeof this.to === 'string') {
39 | return this.$localizePath(this.to);
40 | }
41 | else if (typeof this.to === 'object' && typeof this.to.path === 'string') {
42 | var o = JSON.parse(JSON.stringify(this.to));
43 | o.path = this.$localizePath(o.path);
44 | return o;
45 | }
46 | else {
47 | return this.to;
48 | }
49 | },
50 | },
51 | };
52 |
53 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
54 | if (typeof shadowMode !== 'boolean') {
55 | createInjectorSSR = createInjector;
56 | createInjector = shadowMode;
57 | shadowMode = false;
58 | }
59 | var options = typeof script === 'function' ? script.options : script;
60 | if (template && template.render) {
61 | options.render = template.render;
62 | options.staticRenderFns = template.staticRenderFns;
63 | options._compiled = true;
64 | if (isFunctionalTemplate) {
65 | options.functional = true;
66 | }
67 | }
68 | if (scopeId) {
69 | options._scopeId = scopeId;
70 | }
71 | var hook;
72 | if (moduleIdentifier) {
73 | hook = function (context) {
74 | context =
75 | context ||
76 | (this.$vnode && this.$vnode.ssrContext) ||
77 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext);
78 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
79 | context = __VUE_SSR_CONTEXT__;
80 | }
81 | if (style) {
82 | style.call(this, createInjectorSSR(context));
83 | }
84 | if (context && context._registeredComponents) {
85 | context._registeredComponents.add(moduleIdentifier);
86 | }
87 | };
88 | options._ssrRegister = hook;
89 | }
90 | else if (style) {
91 | hook = shadowMode
92 | ? function (context) {
93 | style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
94 | }
95 | : function (context) {
96 | style.call(this, createInjector(context));
97 | };
98 | }
99 | if (hook) {
100 | if (options.functional) {
101 | var originalRender = options.render;
102 | options.render = function renderWithStyleInjection(h, context) {
103 | hook.call(context);
104 | return originalRender(h, context);
105 | };
106 | }
107 | else {
108 | var existing = options.beforeCreate;
109 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
110 | }
111 | }
112 | return script;
113 | }
114 |
115 | /* script */
116 | var __vue_script__$1 = script$1;
117 |
118 | /* template */
119 | var __vue_render__$1 = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('router-link',_vm._b({attrs:{"to":_vm.localizedTo()}},'router-link',_vm.$attrs,false),[_vm._t("default")],2)};
120 | var __vue_staticRenderFns__$1 = [];
121 |
122 | /* style */
123 | var __vue_inject_styles__$1 = undefined;
124 | /* scoped */
125 | var __vue_scope_id__$1 = undefined;
126 | /* module identifier */
127 | var __vue_module_identifier__$1 = undefined;
128 | /* functional template */
129 | var __vue_is_functional_template__$1 = false;
130 | /* style inject */
131 |
132 | /* style inject SSR */
133 |
134 | /* style inject shadow dom */
135 |
136 |
137 |
138 | var __vue_component__$1 = /*#__PURE__*/normalizeComponent(
139 | { render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
140 | __vue_inject_styles__$1,
141 | __vue_script__$1,
142 | __vue_scope_id__$1,
143 | __vue_is_functional_template__$1,
144 | __vue_module_identifier__$1,
145 | false,
146 | undefined,
147 | undefined,
148 | undefined
149 | );
150 |
151 | var script = {
152 | name: 'LanguageSwitcher',
153 | data: function data () {
154 | return {
155 | currentUrl: this.url || this.$router.currentRoute.fullPath,
156 | links: [],
157 | };
158 | },
159 | props: [ 'tag', 'active-class', 'url' ],
160 | methods: {
161 | getTag: function getTag () {
162 | if (this.tag) { return this.tag; }
163 | else { return 'div'; }
164 | },
165 | generateLinks: function generateLinks () {
166 | var links = [];
167 | var activeClass = this.activeClass || 'router-active-language';
168 | var tr = this._langRouter.translations;
169 | for (var lang in tr) {
170 | if (tr.hasOwnProperty(lang)) {
171 | links.push({
172 | activeClass: (lang == this.$i18n.locale ? activeClass : ''),
173 | langIndex: lang,
174 | langName: tr[lang].name || lang,
175 | url: this.$localizePath(this.currentUrl, lang),
176 | });
177 | }
178 | }
179 | this.links = links;
180 | },
181 | },
182 | watch: {
183 | $route: function $route (to) {
184 | this.currentUrl = this.url || to.fullPath;
185 | this.generateLinks();
186 | },
187 | },
188 | mounted: function mounted () {
189 | this.generateLinks();
190 | },
191 | };
192 |
193 | /* script */
194 | var __vue_script__ = script;
195 |
196 | /* template */
197 | var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c(_vm.getTag(),{tag:"component",staticClass:"router-language-switcher"},[_vm._t("default",null,{"links":_vm.links})],2)};
198 | var __vue_staticRenderFns__ = [];
199 |
200 | /* style */
201 | var __vue_inject_styles__ = undefined;
202 | /* scoped */
203 | var __vue_scope_id__ = undefined;
204 | /* module identifier */
205 | var __vue_module_identifier__ = undefined;
206 | /* functional template */
207 | var __vue_is_functional_template__ = false;
208 | /* style inject */
209 |
210 | /* style inject SSR */
211 |
212 | /* style inject shadow dom */
213 |
214 |
215 |
216 | var __vue_component__ = /*#__PURE__*/normalizeComponent(
217 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
218 | __vue_inject_styles__,
219 | __vue_script__,
220 | __vue_scope_id__,
221 | __vue_is_functional_template__,
222 | __vue_module_identifier__,
223 | false,
224 | undefined,
225 | undefined,
226 | undefined
227 | );
228 |
229 | var defaultLanguage, translations, localizedURLs; exports.i18n = void 0;
230 | var loadedTranslations = [];
231 | function err (msg, error) {
232 | console.error('LangRouter: ' + msg);
233 | if (typeof error !== 'undefined') { console.error(error); }
234 | }
235 | var LangRouter = function LangRouter (options) {
236 | for (var lang in translations) {
237 | if (translations.hasOwnProperty(lang) && !localizedURLs[lang]) {
238 | localizedURLs[lang] = {};
239 | }
240 | }
241 | for (var lang$1 in localizedURLs) {
242 | if (localizedURLs.hasOwnProperty(lang$1)) {
243 | addAliasesToRoutes(options.routes, lang$1);
244 | }
245 | }
246 | var router = new VueRouter__default['default'](options);
247 | router.beforeEach(switchLanguage);
248 | return router;
249 | };
250 | LangRouter.install = function (Vue, options) {
251 | if (LangRouter.installed) {
252 | err('Already installed.');
253 | return;
254 | }
255 | LangRouter.installed = true;
256 | if (!options) {
257 | err('Options missing.');
258 | }
259 | defaultLanguage = options.defaultLanguage;
260 | translations = options.translations;
261 | localizedURLs = options.localizedURLs || {};
262 | var isArr;
263 | if ((isArr = Array.isArray(translations)) || typeof translations !== 'object') {
264 | err('options.translations should be an object, received ' + (isArr ? 'array' : typeof translations) + ' instead.');
265 | }
266 | if ((isArr = Array.isArray(localizedURLs)) || typeof localizedURLs !== 'object') {
267 | err('options.localizedURLs should be an object, received ' + (isArr ? 'array' : typeof localizedURLs) + ' instead.');
268 | }
269 | if (typeof defaultLanguage !== 'string') {
270 | err('options.defaultLanguage should be a string, received ' + typeof defaultLanguage + ' instead.');
271 | }
272 | Vue.use(VueI18n__default['default']);
273 | Vue.use(VueRouter__default['default']);
274 | var messages = {};
275 | for (var lang in translations) {
276 | if (translations.hasOwnProperty(lang)) {
277 | var langMessages = translations[lang].messages;
278 | if (typeof langMessages === 'object' && !Array.isArray(langMessages)) {
279 | messages[lang] = translations[lang].messages;
280 | loadedTranslations.push(lang);
281 | }
282 | }
283 | }
284 | exports.i18n = new VueI18n__default['default'](_spread({}, {locale: defaultLanguage,
285 | fallbackLocale: defaultLanguage,
286 | messages: messages},
287 | options.i18nOptions));
288 | Vue.prototype._langRouter = { translations: translations };
289 | Vue.prototype.$localizePath = localizePath;
290 | Vue.component('localized-link', __vue_component__$1);
291 | Vue.component('language-switcher', __vue_component__);
292 | };
293 | function setLanguage (lang) {
294 | exports.i18n.locale = lang;
295 | document.querySelector('html').setAttribute('lang', lang);
296 | localStorage.setItem('VueAppLanguage', lang);
297 | return lang;
298 | }
299 | function loadLanguage (lang) {
300 | if (loadedTranslations.includes(lang)) {
301 | return Promise.resolve(setLanguage(lang));
302 | }
303 | if (!translations[lang] || typeof translations[lang].load !== 'function') {
304 | err('Unable to load translations for "' + lang + '", "load" function is missing!');
305 | }
306 | return translations[lang].load().then(function (messages) {
307 | exports.i18n.setLocaleMessage(lang, messages.default || messages);
308 | loadedTranslations.push(lang);
309 | return setLanguage(lang);
310 | }).catch(function (error) {
311 | err('Failed to load "' + lang + '" translation.', error);
312 | });
313 | }
314 | function addAliasesToRoutes (routes, lang, child) {
315 | routes.forEach(function (route) {
316 | var alias = translatePath(route.path, lang);
317 | if (!child) { alias = '/' + lang + (alias.charAt(0) != '/' ? '/' : '') + alias; }
318 | if (route.alias) {
319 | if (!Array.isArray(route.alias)) { route.alias = [ route.alias ]; }
320 | }
321 | else { route.alias = []; }
322 | if (route.path != alias && route.alias.indexOf(alias) == -1) { route.alias.push(alias); }
323 | if (route.children) { addAliasesToRoutes(route.children, lang, true); }
324 | });
325 | }
326 | function switchLanguage (to, from, next) {
327 | var lang = to.path.split('/')[1];
328 | if (!translations[lang]) {
329 | var savedLang = localStorage.getItem('VueAppLanguage');
330 | if (savedLang && translations[savedLang]) { lang = savedLang; }
331 | else {
332 | var preferredLang = getPrefferedLanguage();
333 | if (preferredLang && translations[preferredLang]) { lang = preferredLang; }
334 | else { lang = defaultLanguage; }
335 | }
336 | if (lang != defaultLanguage) {
337 | var translatedPath = translatePath(to.path, lang);
338 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
339 | return next({ path: translatedPath, query: to.query, hash: to.hash });
340 | }
341 | }
342 | loadLanguage(lang).then(function () {
343 | return next();
344 | });
345 | }
346 | function translatePath (path, langTo, langFrom, matchedPath) {
347 | var pathChunks = path.split('/');
348 | if (langFrom && localizedURLs[langFrom]) {
349 | if (langTo == langFrom) { return path; }
350 | var map = localizedURLs[langFrom];
351 | var reversedMap = {};
352 | Object.keys(map).forEach(function (key) {
353 | reversedMap[map[key]] = key;
354 | });
355 | var matchedPathChunks = matchedPath.split('/');
356 | for (var i = 0; i < pathChunks.length; i++) {
357 | var pathChunk = pathChunks[i];
358 | if (matchedPathChunks[i].charAt(0) == ':') { continue; }
359 | pathChunks[i] = reversedMap[pathChunk] || pathChunk;
360 | }
361 | }
362 | for (var i$1 = 0; i$1 < pathChunks.length; i$1++) {
363 | var pathChunk$1 = pathChunks[i$1];
364 | if (pathChunk$1.charAt(0) == ':') { continue; }
365 | pathChunks[i$1] = localizedURLs[langTo][pathChunk$1] || pathChunk$1;
366 | }
367 | return pathChunks.join('/');
368 | }
369 | function getPrefferedLanguage () {
370 | function extractLanguage (s) {
371 | return s.split('-')[0].toLowerCase();
372 | }
373 | if (navigator.languages && navigator.languages.length) { return extractLanguage(navigator.languages[0] || ''); }
374 | return extractLanguage(navigator.language || navigator.browserLanguage || navigator.userLanguage || '');
375 | }
376 | function localizePath (fullPath, lang) {
377 | if (!lang || !localizedURLs[lang]) { lang = exports.i18n.locale; }
378 | var path = fullPath;
379 | var query = '';
380 | if (fullPath.includes('?')) {
381 | path = fullPath.split('?')[0];
382 | query = '?' + fullPath.split('?')[1];
383 | }
384 | else if (fullPath.includes('#')) {
385 | path = fullPath.split('#')[0];
386 | query = '#' + fullPath.split('#')[1];
387 | }
388 | var pathChunks = path.split('/');
389 | var pathLang = (localizedURLs[pathChunks[1]] ? pathChunks[1] : false);
390 | var currentPathLang = this.$router.currentRoute.path.split('/')[1];
391 | if (lang == defaultLanguage && !localizedURLs[currentPathLang] && !pathLang) { return fullPath; }
392 | var resolvedPath = false;
393 | if (pathLang) {
394 | var resolvedRoute = this.$router.resolve(path);
395 | if (resolvedRoute.route.matched.length != 0) {
396 | resolvedPath = resolvedRoute.route.matched[resolvedRoute.route.matched.length - 1].path;
397 | resolvedPath = (resolvedPath.charAt(0) == '/' ? resolvedPath : '/' + resolvedPath);
398 | }
399 | else {
400 | err('Router could not resolve path "' + path + '". URL localization may not work as expected.');
401 | }
402 | pathChunks.splice(1, 1);
403 | path = pathChunks.join('/');
404 | }
405 | var translatedPath = translatePath(path, lang, pathLang, (resolvedPath || path));
406 | translatedPath = '/' + lang + (translatedPath.charAt(0) != '/' ? '/' : '') + translatedPath;
407 | return translatedPath + query;
408 | }
409 | if (typeof window !== 'undefined' && window.Vue) {
410 | window.Vue.use(LangRouter);
411 | }
412 |
413 | exports['default'] = LangRouter;
414 |
415 | Object.defineProperty(exports, '__esModule', { value: true });
416 |
417 | })));
418 |
--------------------------------------------------------------------------------