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",
"build": "vue-cli-service build",
"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": {
"axios": "^0.21.1",
@ -18,6 +19,7 @@
"qs": "^6.9.4",
"vue": "^2.6.11",
"vue-cookies": "^1.7.3",
"vue-i18n": "^8.22.4",
"vue-line-clamp": "^1.3.2",
"vue-moment": "^4.1.0",
"vue-read-more-smooth": "^0.1.8",
@ -36,6 +38,7 @@
"@types/lodash": "^4.14.158",
"@types/vuedraggable": "^2.23.1",
"@types/vuelidate": "^0.7.13",
"@types/webpack": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.7.1",
"@typescript-eslint/parser": "^3.7.1",
"@vue/cli-plugin-babel": "^4.4.6",
@ -61,7 +64,9 @@
"ts-jest": "^26.1.4",
"typeface-roboto": "0.0.75",
"typescript": "^3.9.7",
"vue-cli-plugin-i18n": "~1.0.1",
"vue-cli-plugin-vuetify": "^2.0.7",
"vue-i18n-extract": "^1.1.11",
"vue-template-compiler": "^2.6.11",
"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

@ -1,10 +1,10 @@
import _, { LoDashStatic } from 'lodash'
import _, {LoDashStatic} from 'lodash'
import Vue from 'vue'
import VueCookies from 'vue-cookies'
// @ts-ignore
import * as lineClamp from 'vue-line-clamp'
import Vuelidate from 'vuelidate'
import { sync } from 'vuex-router-sync'
import {sync} from 'vuex-router-sync'
import App from './App.vue'
import actuator from './plugins/actuator.plugin'
import httpPlugin from './plugins/http.plugin'
@ -21,6 +21,7 @@ import vuetify from './plugins/vuetify'
import './public-path'
import router from './router'
import store from './store'
import i18n from './i18n'
Vue.use(Vuelidate)
Vue.use(lineClamp)
@ -50,6 +51,7 @@ new Vue({
router,
store,
vuetify,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -20,7 +20,7 @@
<v-container fluid>
<empty-state v-if="allEmpty"
title="Nothing to show"
:title="$t('common.nothing_to_show')"
icon="mdi-help-circle"
icon-color="secondary"
>
@ -28,7 +28,7 @@
<horizontal-scroller v-if="inProgressBooks.length !== 0" class="mb-4">
<template v-slot:prepend>
<div class="title">Keep Reading</div>
<div class="title">{{ $t('dashboard.keep_reading') }}</div>
</template>
<template v-slot:content>
<item-browser :items="inProgressBooks"
@ -43,7 +43,7 @@
<horizontal-scroller v-if="onDeckBooks.length !== 0" class="mb-4">
<template v-slot:prepend>
<div class="title">On Deck</div>
<div class="title">{{ $t('dashboard.on_deck') }}</div>
</template>
<template v-slot:content>
<item-browser :items="onDeckBooks"
@ -58,7 +58,7 @@
<horizontal-scroller v-if="newSeries.length !== 0" class="mb-4">
<template v-slot:prepend>
<div class="title">Recently Added Series</div>
<div class="title">{{ $t('dashboard.recently_added_series') }}</div>
</template>
<template v-slot:content>
<item-browser :items="newSeries"
@ -73,7 +73,7 @@
<horizontal-scroller v-if="updatedSeries.length !== 0" class="mb-4">
<template v-slot:prepend>
<div class="title">Recently Updated Series</div>
<div class="title">{{ $t('dashboard.recently_updated_series') }}</div>
</template>
<template v-slot:content>
<item-browser :items="updatedSeries"
@ -88,7 +88,7 @@
<horizontal-scroller v-if="latestBooks.length !== 0" class="mb-4">
<template v-slot:prepend>
<div class="title">Recently Added Books</div>
<div class="title">{{ $t('dashboard.recently_added_books') }}</div>
</template>
<template v-slot:content>
<item-browser :items="latestBooks"
@ -110,9 +110,9 @@ import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue'
import EmptyState from '@/components/EmptyState.vue'
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import { ReadStatus } from '@/types/enum-books'
import { BookDto } from '@/types/komga-books'
import { BOOK_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events'
import {ReadStatus} from '@/types/enum-books'
import {BookDto} from '@/types/komga-books'
import {BOOK_CHANGED, LIBRARY_DELETED, SERIES_CHANGED} from '@/types/events'
import Vue from 'vue'
import {SeriesDto} from "@/types/komga-series";

View File

@ -114,6 +114,18 @@
</v-list-item>
</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/>
<template v-slot:append>
@ -136,10 +148,11 @@
import Dialogs from '@/components/Dialogs.vue'
import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
import SearchBox from '@/components/SearchBox.vue'
import { Theme } from '@/types/themes'
import {Theme} from '@/types/themes'
import Vue from 'vue'
const cookieTheme = 'theme'
const cookieLocale = 'locale'
export default Vue.extend({
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)
},
async beforeDestroy () {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.systemThemeChange)
},
computed: {
locales (): string[] {
return this.$i18n.availableLocales
},
libraries (): LibraryDto[] {
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: {
toggleDrawer () {

View File

@ -13,7 +13,9 @@
"types": [
"webpack-env",
"jest",
"vuetify"
"vuetify",
"webpack",
"webpack-env"
],
"paths": {
"@/*": [
@ -37,4 +39,4 @@
"exclude": [
"node_modules"
]
}
}

View File

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