diff options
-rw-r--r-- | package-lock.json | 131 | ||||
-rw-r--r-- | package.json | 10 | ||||
-rw-r--r-- | src/App.vue | 2 | ||||
-rw-r--r-- | src/components/Global/PageTitle.vue | 8 | ||||
-rw-r--r-- | src/i18n.js | 29 | ||||
-rw-r--r-- | src/locales/en.json | 38 | ||||
-rw-r--r-- | src/locales/es.json | 38 | ||||
-rw-r--r-- | src/main.js | 2 | ||||
-rw-r--r-- | src/router/index.js | 10 | ||||
-rw-r--r-- | src/views/Login/Login.vue | 42 | ||||
-rw-r--r-- | src/views/Overview/OverviewQuickLinks.vue | 2 | ||||
-rw-r--r-- | vue.config.js | 8 |
12 files changed, 291 insertions, 29 deletions
diff --git a/package-lock.json b/package-lock.json index 030b9154..d07b4e7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1198,6 +1198,12 @@ "@types/yargs": "^13.0.0" } }, + "@kazupon/vue-i18n-loader": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@kazupon/vue-i18n-loader/-/vue-i18n-loader-0.3.0.tgz", + "integrity": "sha512-M2280E9PMxetu6mOdtyh1d6Dif7LwH4gvxD2dgsu7HOyzR26AUNok8DxZ1Y5YAexJvPfbBXC75Llui2myO05Hg==", + "dev": true + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -4362,6 +4368,44 @@ "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==", "dev": true }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -4548,6 +4592,13 @@ "simple-swizzle": "^0.2.2" } }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "optional": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5862,6 +5913,16 @@ "domelementtype": "1" } }, + "dot-object": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-1.9.0.tgz", + "integrity": "sha512-7MPN6y7XhAO4vM4eguj5+5HNKLjJYfkVG1ZR1Aput4Q4TR6SYeSjhpVQ77IzJHoSHffKbDxBC+48aCiiRurDPw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "glob": "^7.1.4" + } + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -6319,6 +6380,12 @@ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, "espree": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", @@ -6878,6 +6945,15 @@ "locate-path": "^3.0.0" } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -8990,6 +9066,12 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true + }, "is-whitespace": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", @@ -15993,6 +16075,36 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" }, + "vue-cli-plugin-i18n": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/vue-cli-plugin-i18n/-/vue-cli-plugin-i18n-0.6.1.tgz", + "integrity": "sha512-/2T/T47x8Aj3xLfrNYe5L1pzw+TnoDdKahcQflsPPjkXCuDss4rRVXVH0zzI3gpx2B1s9WW+yMYsg0JUlxgGEw==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "deepmerge": "^2.1.1", + "dotenv": "^6.0.0", + "flat": "^4.0.0", + "rimraf": "^2.6.3", + "vue": "^2.5.16", + "vue-i18n": "^8.0.0", + "vue-i18n-extract": "^1.0.2" + }, + "dependencies": { + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true + }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", + "dev": true + } + } + }, "vue-date-fns": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vue-date-fns/-/vue-date-fns-1.1.0.tgz", @@ -16054,6 +16166,25 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, + "vue-i18n": { + "version": "8.15.3", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz", + "integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew==" + }, + "vue-i18n-extract": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vue-i18n-extract/-/vue-i18n-extract-1.0.2.tgz", + "integrity": "sha512-+zwDKvle4KcfloXZnj5hF01ViKDiFr5RMx5507D7oyDXpSleRpekF5YHgZa/+Ra6Go68//z0Nya58J9tKFsCjw==", + "dev": true, + "requires": { + "cli-table3": "^0.5.1", + "dot-object": "^1.7.1", + "esm": "^3.2.13", + "glob": "^7.1.3", + "is-valid-glob": "^1.0.0", + "yargs": "^13.2.2" + } + }, "vue-jest": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-3.0.5.tgz", diff --git a/package.json b/package.json index 188545db..361adb2b 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { "name": "webui-vue", - "description": "OpenBMC Web UI using the Vue.js front-end framework", "version": "0.1.0", "private": true, + "description": "OpenBMC Web UI using the Vue.js front-end framework", "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint", "docs:serve": "vuepress dev docs", - "docs:build": "vuepress build docs" + "docs:build": "vuepress build docs", + "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'" }, "dependencies": { "@carbon/icons-vue": "10.6.1", @@ -20,11 +21,13 @@ "js-cookie": "^2.2.1", "vue": "2.6.11", "vue-date-fns": "1.1.0", + "vue-i18n": "8.0.0", "vue-router": "3.1.3", "vuelidate": "^0.7.4", "vuex": "3.0.1" }, "devDependencies": { + "@kazupon/vue-i18n-loader": "0.3.0", "@vue/cli-plugin-babel": "4.0.0", "@vue/cli-plugin-eslint": "4.0.5", "@vue/cli-plugin-router": "4.0.0", @@ -43,7 +46,8 @@ "prettier": "1.18.2", "sass-loader": "8.0.0", "vue-template-compiler": "2.6.11", - "vuepress": "^1.2.0" + "vuepress": "^1.2.0", + "vue-cli-plugin-i18n": "0.6.1" }, "gitHooks": { "pre-commit": "lint-staged" diff --git a/src/App.vue b/src/App.vue index a5a768a5..30de752b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,7 +13,7 @@ export default { name: 'App', watch: { $route: function(to) { - document.title = to.meta.title || 'Page is Missing Title'; + document.title = this.$t(to.meta.title) || 'Page is missing title'; } } }; diff --git a/src/components/Global/PageTitle.vue b/src/components/Global/PageTitle.vue index 5c64f0d6..59bb6a1e 100644 --- a/src/components/Global/PageTitle.vue +++ b/src/components/Global/PageTitle.vue @@ -14,10 +14,10 @@ export default { default: '' } }, - data() { - return { - title: this.$route.meta.title - }; + computed: { + title() { + return this.$t(this.$route.meta.title); + } } }; </script> diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 00000000..09b3f4ca --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; + +Vue.use(VueI18n); + +function loadLocaleMessages() { + const locales = require.context( + './locales', + true, + /[A-Za-z0-9-_,\s]+\.json$/i + ); + const messages = {}; + locales.keys().forEach(key => { + const matched = key.match(/([A-Za-z0-9-_]+)\./i); + if (matched && matched.length > 1) { + const locale = matched[1]; + messages[locale] = locales(key); + } + }); + return messages; +} + +export default new VueI18n({ + // default language is English + locale: 'en', + // locale messages with a message key that doesn't exist will fallback to English + fallbackLocale: 'en', + messages: loadLocaleMessages() +}); diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 00000000..8464ff43 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,38 @@ +{ + "global": { + "formField": { + "validator": "Field required" + } + }, + "login": { + "language": { + "label": "Language" + }, + "languages": { + "select": "Select an option", + "english": "English", + "spanish": "Spanish" + }, + "logIn": { + "label": "Log in" + }, + "errorMsg": { + "title": "Invalid username or password.", + "action": "Please try again." + }, + "password": { + "label": "Password", + "validator": "@:global.formField.validator" + }, + "username": { + "label": "Username", + "validator": "@:global.formField.validator" + } + }, + "pageTitle": { + "localUserMgmt": "Local user management", + "login": "Login", + "overview": "Overview", + "unauthorized": "Unauthorized" + } +}
\ No newline at end of file diff --git a/src/locales/es.json b/src/locales/es.json new file mode 100644 index 00000000..30d1fd17 --- /dev/null +++ b/src/locales/es.json @@ -0,0 +1,38 @@ +{ + "global": { + "formField": { + "validator": "Campo requerido" + } + }, + "login": { + "language": { + "label": "Idioma" + }, + "languages": { + "select": "Seleccione una opción", + "english": "Inglés", + "spanish": "Español" + }, + "logIn": { + "label": "Iniciar sesión" + }, + "errorMsg": { + "title": "Usuario o contraseña invalido.", + "action": "Inténtalo de nuevo." + }, + "password": { + "label": "Contraseña", + "validator": "@:global.formField.validator" + }, + "username": { + "label": "Nombre de usuario", + "validator": "@:global.formField.validator" + } + }, + "pageTitle": { + "localUserMgmt": "Administración de usuarios locales", + "login": "Inicio de sesión", + "overview": "Información general", + "unauthorized": "No autorizado" + } +}
\ No newline at end of file diff --git a/src/main.js b/src/main.js index d80d2019..72167510 100644 --- a/src/main.js +++ b/src/main.js @@ -25,6 +25,7 @@ import { ToastPlugin } from 'bootstrap-vue'; import Vuelidate from 'vuelidate'; +import i18n from './i18n'; Vue.filter('date', dateFilter); @@ -59,5 +60,6 @@ Vue.use(Vuelidate); new Vue({ router, store, + i18n, render: h => h(App) }).$mount('#app'); diff --git a/src/router/index.js b/src/router/index.js index 71b90fb1..bec7f548 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -5,6 +5,8 @@ import AppLayout from '../layouts/AppLayout.vue'; Vue.use(VueRouter); +// Meta title is translated using i18n in App.vue and PageTitle.Vue +// Example meta: {title: 'pageTitle.overview'} const routes = [ { path: '/', @@ -18,7 +20,7 @@ const routes = [ path: '', component: () => import('@/views/Overview'), meta: { - title: 'Overview' + title: 'pageTitle.overview' } }, { @@ -26,7 +28,7 @@ const routes = [ name: 'local-users', component: () => import('@/views/AccessControl/LocalUserManagement'), meta: { - title: 'Local user management' + title: 'pageTitle.localUserMgmt' } }, { @@ -34,7 +36,7 @@ const routes = [ name: 'unauthorized', component: () => import('@/views/Unauthorized'), meta: { - title: 'Unauthorized' + title: 'pageTitle.unauthorized' } } ] @@ -44,7 +46,7 @@ const routes = [ name: 'login', component: () => import('@/views/Login'), meta: { - title: 'Login' + title: 'pageTitle.login' } } ]; diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue index 35af76f2..d4fde8cb 100644 --- a/src/views/Login/Login.vue +++ b/src/views/Login/Login.vue @@ -13,17 +13,24 @@ <h1>OpenBMC</h1> </div> </b-col> - <b-col md="6"> <b-form class="login-form" novalidate @submit.prevent="login"> <b-alert class="login-error" :show="authError" variant="danger"> <p id="login-error-alert"> - <strong>{{ errorMsg.title }}</strong> - <span>{{ errorMsg.action }}</span> + <strong>{{ $t('login.errorMsg.title') }}</strong> + <span>{{ $t('login.errorMsg.action') }}</span> </p> </b-alert> <div class="login-form__section"> - <label for="username">Username</label> + <label for="language">{{ $t('login.language.label') }}</label> + <b-form-select + id="language" + v-model="$i18n.locale" + :options="languages" + ></b-form-select> + </div> + <div class="login-form__section"> + <label for="username">{{ $t('login.username.label') }}</label> <b-form-input id="username" v-model="userInfo.username" @@ -36,13 +43,12 @@ </b-form-input> <b-form-invalid-feedback role="alert"> <template v-if="!$v.userInfo.username.required"> - Field required + {{ $t('login.username.validator') }} </template> </b-form-invalid-feedback> </div> - <div class="login-form__section"> - <label for="password">Password</label> + <label for="password">{{ $t('login.password.label') }}</label> <b-form-input id="password" v-model="userInfo.password" @@ -54,18 +60,17 @@ </b-form-input> <b-form-invalid-feedback role="alert"> <template v-if="!$v.userInfo.password.required"> - Field required + {{ $t('login.password.validator') }} </template> </b-form-invalid-feedback> </div> - <b-button block class="mt-5" type="submit" variant="primary" :disabled="disableSubmitButton" - >Log in</b-button + >{{ $t('login.logIn.label') }}</b-button > </b-form> </b-col> @@ -83,15 +88,22 @@ export default { mixins: [VuelidateMixin], data() { return { - errorMsg: { - title: 'Invalid username or password.', - action: 'Please try again.' - }, userInfo: { username: null, password: null }, - disableSubmitButton: false + disableSubmitButton: false, + languages: [ + { value: null, text: this.$t('login.languages.select') }, + { + value: 'en', + text: this.$t('login.languages.english') + }, + { + value: 'es', + text: this.$t('login.languages.spanish') + } + ] }; }, computed: { diff --git a/src/views/Overview/OverviewQuickLinks.vue b/src/views/Overview/OverviewQuickLinks.vue index d9d86ca8..89253977 100644 --- a/src/views/Overview/OverviewQuickLinks.vue +++ b/src/views/Overview/OverviewQuickLinks.vue @@ -43,7 +43,7 @@ export default { }, data() { return { - serverLEDChecked: false + serverLedChecked: false }; }, computed: { diff --git a/vue.config.js b/vue.config.js index e40b01ef..12a723d6 100644 --- a/vue.config.js +++ b/vue.config.js @@ -16,7 +16,7 @@ module.exports = { '/': { target: process.env.BASE_URL, onProxyRes: proxyRes => { - // This header is igorned in the browser so removing + // This header is ignored in the browser so removing // it so we don't see warnings in the browser console delete proxyRes.headers['strict-transport-security']; } @@ -39,5 +39,11 @@ module.exports = { config.plugins.delete('prefetch'); config.plugins.delete('preload'); } + }, + pluginOptions: { + i18n: { + localeDir: 'locales', + enableInSFC: true + } } }; |