feat(webui): support for translations

closes #187
This commit is contained in:
Gauthier Roebroeck 2021-02-08 16:42:28 +08:00
parent 75019c9a1e
commit efe6476a90
11 changed files with 372 additions and 20689 deletions

2
komga-webui/.env Normal file
View file

@ -0,0 +1,2 @@
VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@
"serve": "vue-cli-service serve --port 8081", "serve": "vue-cli-service serve --port 8081",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint --mode production" "lint": "vue-cli-service lint --mode production",
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -18,6 +19,7 @@
"qs": "^6.9.4", "qs": "^6.9.4",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-cookies": "^1.7.3", "vue-cookies": "^1.7.3",
"vue-i18n": "^8.22.4",
"vue-line-clamp": "^1.3.2", "vue-line-clamp": "^1.3.2",
"vue-moment": "^4.1.0", "vue-moment": "^4.1.0",
"vue-read-more-smooth": "^0.1.8", "vue-read-more-smooth": "^0.1.8",
@ -36,6 +38,7 @@
"@types/lodash": "^4.14.158", "@types/lodash": "^4.14.158",
"@types/vuedraggable": "^2.23.1", "@types/vuedraggable": "^2.23.1",
"@types/vuelidate": "^0.7.13", "@types/vuelidate": "^0.7.13",
"@types/webpack": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.7.1", "@typescript-eslint/eslint-plugin": "^3.7.1",
"@typescript-eslint/parser": "^3.7.1", "@typescript-eslint/parser": "^3.7.1",
"@vue/cli-plugin-babel": "^4.4.6", "@vue/cli-plugin-babel": "^4.4.6",
@ -61,7 +64,9 @@
"ts-jest": "^26.1.4", "ts-jest": "^26.1.4",
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"vue-cli-plugin-i18n": "~1.0.1",
"vue-cli-plugin-vuetify": "^2.0.7", "vue-cli-plugin-vuetify": "^2.0.7",
"vue-i18n-extract": "^1.1.11",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.6.0" "vuetify-loader": "^1.6.0"
} }

23
komga-webui/src/i18n.ts Normal file
View file

@ -0,0 +1,23 @@
import Vue from 'vue'
import VueI18n, {LocaleMessages} from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages (): LocaleMessages {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages: LocaleMessages = {}
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({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages(),
})

View file

@ -0,0 +1,12 @@
{
"dashboard": {
"recently_added_series": "Recently Added Series",
"on_deck": "On Deck",
"keep_reading": "Keep Reading",
"recently_updated_series": "Recently Updated Series",
"recently_added_books": "Recently Added Books"
},
"common": {
"nothing_to_show": "Nothing to show"
}
}

View file

@ -0,0 +1,5 @@
{
"dashboard": {
"recently_added_series": "Séries ajoutées récemment"
}
}

View file

@ -21,6 +21,7 @@ import vuetify from './plugins/vuetify'
import './public-path' import './public-path'
import router from './router' import router from './router'
import store from './store' import store from './store'
import i18n from './i18n'
Vue.use(Vuelidate) Vue.use(Vuelidate)
Vue.use(lineClamp) Vue.use(lineClamp)
@ -50,6 +51,7 @@ new Vue({
router, router,
store, store,
vuetify, vuetify,
i18n,
render: h => h(App), render: h => h(App),
}).$mount('#app') }).$mount('#app')

View file

@ -20,7 +20,7 @@
<v-container fluid> <v-container fluid>
<empty-state v-if="allEmpty" <empty-state v-if="allEmpty"
title="Nothing to show" :title="$t('common.nothing_to_show')"
icon="mdi-help-circle" icon="mdi-help-circle"
icon-color="secondary" icon-color="secondary"
> >
@ -28,7 +28,7 @@
<horizontal-scroller v-if="inProgressBooks.length !== 0" class="mb-4"> <horizontal-scroller v-if="inProgressBooks.length !== 0" class="mb-4">
<template v-slot:prepend> <template v-slot:prepend>
<div class="title">Keep Reading</div> <div class="title">{{ $t('dashboard.keep_reading') }}</div>
</template> </template>
<template v-slot:content> <template v-slot:content>
<item-browser :items="inProgressBooks" <item-browser :items="inProgressBooks"
@ -43,7 +43,7 @@
<horizontal-scroller v-if="onDeckBooks.length !== 0" class="mb-4"> <horizontal-scroller v-if="onDeckBooks.length !== 0" class="mb-4">
<template v-slot:prepend> <template v-slot:prepend>
<div class="title">On Deck</div> <div class="title">{{ $t('dashboard.on_deck') }}</div>
</template> </template>
<template v-slot:content> <template v-slot:content>
<item-browser :items="onDeckBooks" <item-browser :items="onDeckBooks"
@ -58,7 +58,7 @@
<horizontal-scroller v-if="newSeries.length !== 0" class="mb-4"> <horizontal-scroller v-if="newSeries.length !== 0" class="mb-4">
<template v-slot:prepend> <template v-slot:prepend>
<div class="title">Recently Added Series</div> <div class="title">{{ $t('dashboard.recently_added_series') }}</div>
</template> </template>
<template v-slot:content> <template v-slot:content>
<item-browser :items="newSeries" <item-browser :items="newSeries"
@ -73,7 +73,7 @@
<horizontal-scroller v-if="updatedSeries.length !== 0" class="mb-4"> <horizontal-scroller v-if="updatedSeries.length !== 0" class="mb-4">
<template v-slot:prepend> <template v-slot:prepend>
<div class="title">Recently Updated Series</div> <div class="title">{{ $t('dashboard.recently_updated_series') }}</div>
</template> </template>
<template v-slot:content> <template v-slot:content>
<item-browser :items="updatedSeries" <item-browser :items="updatedSeries"
@ -88,7 +88,7 @@
<horizontal-scroller v-if="latestBooks.length !== 0" class="mb-4"> <horizontal-scroller v-if="latestBooks.length !== 0" class="mb-4">
<template v-slot:prepend> <template v-slot:prepend>
<div class="title">Recently Added Books</div> <div class="title">{{ $t('dashboard.recently_added_books') }}</div>
</template> </template>
<template v-slot:content> <template v-slot:content>
<item-browser :items="latestBooks" <item-browser :items="latestBooks"

View file

@ -114,6 +114,18 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-list>
<v-list-item>
<v-list-item-icon>
<v-icon>mdi-translate</v-icon>
</v-list-item-icon>
<v-select v-model="locale"
:items="$i18n.availableLocales"
>
</v-select>
</v-list-item>
</v-list>
<v-spacer/> <v-spacer/>
<template v-slot:append> <template v-slot:append>
@ -140,6 +152,7 @@ import { Theme } from '@/types/themes'
import Vue from 'vue' import Vue from 'vue'
const cookieTheme = 'theme' const cookieTheme = 'theme'
const cookieLocale = 'locale'
export default Vue.extend({ export default Vue.extend({
name: 'home', name: 'home',
@ -171,12 +184,22 @@ export default Vue.extend({
} }
} }
if (this.$cookies.isKey(cookieLocale)) {
const locale = this.$cookies.get(cookieLocale)
if (this.$i18n.availableLocales.includes(locale)) {
this.$i18n.locale = locale
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.systemThemeChange) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.systemThemeChange)
}, },
async beforeDestroy () { async beforeDestroy () {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.systemThemeChange) window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.systemThemeChange)
}, },
computed: { computed: {
locales (): string[] {
return this.$i18n.availableLocales
},
libraries (): LibraryDto[] { libraries (): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries return this.$store.state.komgaLibraries.libraries
}, },
@ -196,6 +219,17 @@ export default Vue.extend({
} }
}, },
}, },
locale: {
get: function (): string {
return this.$i18n.locale
},
set: function (locale: string): void {
if (this.$i18n.availableLocales.includes(locale)) {
this.$i18n.locale = locale
this.$cookies.set(cookieLocale, locale, Infinity)
}
},
},
}, },
methods: { methods: {
toggleDrawer () { toggleDrawer () {

View file

@ -13,7 +13,9 @@
"types": [ "types": [
"webpack-env", "webpack-env",
"jest", "jest",
"vuetify" "vuetify",
"webpack",
"webpack-env"
], ],
"paths": { "paths": {
"@/*": [ "@/*": [

View file

@ -5,6 +5,7 @@ const _ = require('lodash')
// vue.config.js // vue.config.js
module.exports = { module.exports = {
publicPath: '/', publicPath: '/',
chainWebpack: (config) => { chainWebpack: (config) => {
config.plugins.delete('prefetch') // conflicts with htmlInject config.plugins.delete('prefetch') // conflicts with htmlInject
config.plugins.delete('preload') // conflicts with htmlInject config.plugins.delete('preload') // conflicts with htmlInject
@ -34,4 +35,13 @@ module.exports = {
config.plugin('momentLocalesPlugin') config.plugin('momentLocalesPlugin')
.use(momentLocalesPlugin) .use(momentLocalesPlugin)
}, },
pluginOptions: {
i18n: {
locale: 'en',
fallbackLocale: 'en',
localeDir: 'locales',
enableInSFC: false,
},
},
} }