feat: admin language switcher

pull/6775/head
NGPixel 1 year ago
parent e89f304ddb
commit 1522b26997
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -5,7 +5,7 @@ export default {
Query: {
async locales(obj, args, context, info) {
let remoteLocales = await WIKI.cache.get('locales')
let localLocales = await WIKI.db.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'completeness')
let localLocales = await WIKI.db.locales.query().select('code', 'isRTL', 'language', 'name', 'nativeName', 'createdAt', 'updatedAt', 'completeness')
remoteLocales = remoteLocales || localLocales
return _.map(remoteLocales, rl => {
let isInstalled = _.some(localLocales, ['code', rl.code])

@ -31,6 +31,7 @@ type LocalizationLocale {
installDate: Date
isInstalled: Boolean
isRTL: Boolean
language: String
name: String
nativeName: String
region: String

@ -5,15 +5,16 @@ router-view
<script setup>
import { nextTick, onMounted, reactive, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import { setCssVar, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import gql from 'graphql-tag'
import '@mdi/font/css/materialdesignicons.css'
import { useCommonStore } from './stores/common'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
/* global siteConfig */
// QUASAR
@ -22,6 +23,7 @@ const $q = useQuasar()
// STORES
const commonStore = useCommonStore()
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
const userStore = useUserStore()
@ -54,6 +56,25 @@ watch(() => userStore.cvd, () => {
applyTheme()
})
watch(() => commonStore.locale, applyLocale)
// LOCALE
async function applyLocale (locale) {
if (!i18n.availableLocales.includes(locale)) {
try {
i18n.setLocaleMessage(locale, await commonStore.fetchLocaleStrings(locale))
} catch (err) {
$q.notify({
type: 'negative',
message: `Failed to load ${locale} locale strings.`,
caption: err.message
})
}
}
i18n.locale.value = locale
}
// THEME
async function applyTheme () {
@ -89,35 +110,6 @@ async function applyTheme () {
}
}
// LOCALE
async function fetchLocaleStrings (locale) {
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query fetchLocaleStrings (
$locale: String!
) {
localeStrings (
locale: $locale
)
}
`,
fetchPolicy: 'cache-first',
variables: {
locale
}
})
return resp?.data?.localeStrings
} catch (err) {
console.warn(err)
$q.notify({
type: 'negative',
message: 'Failed to load locale strings.'
})
}
}
// INIT SITE STORE
if (typeof siteConfig !== 'undefined') {
@ -128,30 +120,40 @@ if (typeof siteConfig !== 'undefined') {
applyTheme()
}
// ROUTE GUARDS
router.beforeEach(async (to, from) => {
siteStore.routerLoading = true
// System Flags
commonStore.routerLoading = true
// -> System Flags
if (!flagsStore.loaded) {
flagsStore.load()
}
// Site Info
// -> Site Info
if (!siteStore.id) {
console.info('No pre-cached site config. Loading site info...')
await siteStore.loadSite(window.location.hostname)
console.info(`Using Site ID ${siteStore.id}`)
}
// Locales
if (!i18n.availableLocales.includes('en')) {
i18n.setLocaleMessage('en', await fetchLocaleStrings('en'))
// -> Locale
if (!commonStore.desiredLocale || !siteStore.locales.active.includes(commonStore.desiredLocale)) {
commonStore.setLocale(siteStore.locales.primary)
} else {
applyLocale(commonStore.desiredLocale)
}
// User Auth
// -> User Auth
await userStore.refreshAuth()
// User Profile
// -> User Profile
if (userStore.authenticated && !userStore.profileLoaded) {
console.info(`Refreshing user ${userStore.id} profile...`)
await userStore.refreshProfile()
}
// Page Permissions
// -> Page Permissions
await userStore.fetchPagePermissions(to.path)
})
@ -177,7 +179,7 @@ router.afterEach(() => {
applyTheme()
document.querySelector('.init-loading').remove()
}
siteStore.routerLoading = false
commonStore.routerLoading = false
})
</script>

@ -3,10 +3,14 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context'
import { createUploadLink } from 'apollo-upload-client'
export default boot(({ app, store }) => {
import { useUserStore } from 'src/stores/user'
export default boot(({ app }) => {
const userStore = useUserStore()
// Authentication Link
const authLink = setContext(async (req, { headers }) => {
const token = store.state.value.user.token
const token = userStore.token
return {
headers: {
...headers,

@ -1,10 +1,14 @@
import { boot } from 'quasar/wrappers'
import { createI18n } from 'vue-i18n'
import { useCommonStore } from 'src/stores/common'
export default boot(({ app }) => {
const commonStore = useCommonStore()
const i18n = createI18n({
legacy: false,
locale: 'en',
locale: commonStore.locale || 'en',
fallbackLocale: 'en',
fallbackWarn: false,
messages: {}

@ -68,7 +68,7 @@ q-header.bg-header.text-white.site-header(
q-space
transition(name='syncing')
q-spinner-tail(
v-show='siteStore.routerLoading'
v-show='commonStore.routerLoading'
color='accent'
size='24px'
)
@ -130,6 +130,7 @@ import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { reactive } from 'vue'
import { useCommonStore } from 'src/stores/common'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
@ -139,6 +140,7 @@ const $q = useQuasar()
// STORES
const commonStore = useCommonStore()
const siteStore = useSiteStore()
const userStore = useUserStore()

@ -18,11 +18,25 @@ q-layout.admin(view='hHh Lpr lff')
q-space
transition(name='syncing')
q-spinner-tail(
v-show='siteStore.routerLoading'
v-show='commonStore.routerLoading'
color='accent'
size='24px'
)
q-btn.q-ml-md(flat, dense, icon='las la-times-circle', label='Exit' color='pink', to='/')
q-btn.q-ml-md(flat, dense, icon='las la-times-circle', :label='t(`common.actions.exit`)' color='pink', to='/')
q-btn.q-ml-md(flat, dense, icon='las la-language', :label='commonStore.locale' color='grey-4')
q-menu.translucent-menu(auto-close, anchor='bottom right', self='top right')
q-list(separator)
q-item(
v-for='lang of adminStore.locales'
clickable
@click='commonStore.setLocale(lang.code)'
)
q-item-section(side)
q-avatar(rounded, :color='lang.code === commonStore.locale ? `secondary` : `primary`', text-color='white', size='sm')
.text-caption.text-uppercase: strong {{ lang.language }}
q-item-section
q-item-label {{ lang.name }}
q-item-label(caption) {{ lang.nativeName }}
account-menu
q-drawer.admin-sidebar(v-model='leftDrawerOpen', show-if-above, bordered)
q-scroll-area.admin-nav(
@ -108,6 +122,9 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-ssd.svg')
q-item-section {{ t('admin.storage.title') }}
q-item-section(side)
//- TODO: Reflect site storage status
status-light(:color='true ? `positive` : `warning`', :pulse='false')
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-paint-roller.svg')
@ -159,7 +176,7 @@ q-layout.admin(view='hHh Lpr lff')
q-icon(name='img:/_assets/icons/fluent-message-settings.svg')
q-item-section {{ t('admin.mail.title') }}
q-item-section(side)
status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`')
status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`', :pulse='!adminStore.info.isMailConfigured')
q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg')
@ -169,7 +186,7 @@ q-layout.admin(view='hHh Lpr lff')
q-icon(name='img:/_assets/icons/fluent-bot.svg')
q-item-section {{ t('admin.scheduler.title') }}
q-item-section(side)
status-light(:color='adminStore.info.isSchedulerHealthy ? `positive` : `warning`')
status-light(:color='adminStore.info.isSchedulerHealthy ? `positive` : `warning`', :pulse='!adminStore.info.isSchedulerHealthy')
q-item(to='/_admin/security', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-protect.svg')
@ -223,6 +240,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAdminStore } from 'src/stores/admin'
import { useCommonStore } from 'src/stores/common'
import { useFlagsStore } from 'src/stores/flags'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
@ -244,6 +262,7 @@ const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const commonStore = useCommonStore()
const flagsStore = useFlagsStore()
const siteStore = useSiteStore()
const userStore = useUserStore()
@ -266,13 +285,6 @@ useMeta({
// DATA
const leftDrawerOpen = ref(true)
const overlayIsShown = ref(false)
const search = ref('')
const user = reactive({
name: 'John Doe',
email: 'test@example.com',
picture: null
})
const thumbStyle = {
right: '1px',
borderRadius: '5px',
@ -292,6 +304,9 @@ const siteSectionShown = computed(() => {
const usersSectionShown = computed(() => {
return userStore.can('manage:groups') || userStore.can('manage:users')
})
const overlayIsShown = computed(() => {
return Boolean(adminStore.overlay)
})
// WATCHERS
@ -308,9 +323,6 @@ watch(() => adminStore.sites, (newValue) => {
})
}
})
watch(() => adminStore.overlay, (newValue) => {
overlayIsShown.value = !!newValue
})
watch(() => adminStore.currentSiteId, (newValue) => {
if (newValue && route.params.siteid !== newValue) {
router.push({ params: { siteid: newValue } })
@ -325,6 +337,7 @@ onMounted(async () => {
return
}
adminStore.fetchLocales()
await adminStore.fetchSites()
if (route.params.siteid) {
adminStore.$patch({

@ -35,24 +35,20 @@ export const useAdminStore = defineStore('admin', {
}
},
actions: {
async fetchSites () {
async fetchLocales () {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSites {
sites {
id
hostname
isEnabled
title
query getAdminLocales {
locales {
code
language
name
nativeName
}
}
`,
fetchPolicy: 'network-only'
`
})
this.sites = cloneDeep(resp?.data?.sites ?? [])
if (!this.currentSiteId) {
this.currentSiteId = this.sites[0].id
}
this.locales = cloneDeep(resp?.data?.locales ?? [])
},
async fetchInfo () {
const resp = await APOLLO_CLIENT.query({
@ -78,6 +74,25 @@ export const useAdminStore = defineStore('admin', {
this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)
this.info.isMailConfigured = clone(resp?.data?.systemInfo?.isMailConfigured ?? false)
this.info.isSchedulerHealthy = clone(resp?.data?.systemInfo?.isSchedulerHealthy ?? false)
},
async fetchSites () {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSites {
sites {
id
hostname
isEnabled
title
}
}
`,
fetchPolicy: 'network-only'
})
this.sites = cloneDeep(resp?.data?.sites ?? [])
if (!this.currentSiteId) {
this.currentSiteId = this.sites[0].id
}
}
}
})

@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import gql from 'graphql-tag'
export const useCommonStore = defineStore('common', {
state: () => ({
routerLoading: false,
locale: localStorage.getItem('locale') || 'en',
desiredLocale: localStorage.getItem('locale')
}),
getters: {},
actions: {
async fetchLocaleStrings (locale) {
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query fetchLocaleStrings (
$locale: String!
) {
localeStrings (
locale: $locale
)
}
`,
fetchPolicy: 'cache-first',
variables: {
locale
}
})
return resp?.data?.localeStrings
} catch (err) {
console.warn(err)
throw err
}
},
setLocale (locale) {
this.$patch({
locale,
desiredLocale: locale
})
localStorage.setItem('locale', locale)
}
}
})

@ -6,9 +6,7 @@ import { useUserStore } from './user'
export const useSiteStore = defineStore('site', {
state: () => ({
routerLoading: false,
id: null,
useLocales: false,
hostname: '',
company: '',
contentLicense: '',
@ -39,6 +37,10 @@ export const useSiteStore = defineStore('site', {
markdown: false,
wysiwyg: false
},
locales: {
primary: 'en',
active: ['en']
},
theme: {
dark: false,
injectCSS: '',
@ -84,6 +86,9 @@ export const useSiteStore = defineStore('site', {
opacity: isDark ? 0.25 : 1
}
}
},
useLocales: (state) => {
return state.locales?.active?.length > 1
}
},
actions: {
@ -104,20 +109,9 @@ export const useSiteStore = defineStore('site', {
hostname: $hostname
exact: false
) {
id
hostname
title
description
logoText
company
contentLicense
footerExtra
features {
profile
ratingsMode
reasonForChange
search
}
description
editors {
asciidoc {
isActive
@ -129,6 +123,20 @@ export const useSiteStore = defineStore('site', {
isActive
}
}
features {
profile
ratingsMode
reasonForChange
search
}
footerExtra
hostname
id
locales {
primary
active
}
logoText
theme {
dark
colorPrimary
@ -144,6 +152,7 @@ export const useSiteStore = defineStore('site', {
baseFont
contentFont
}
title
}
}
`,
@ -171,6 +180,10 @@ export const useSiteStore = defineStore('site', {
markdown: clone(siteInfo.editors.markdown.isActive),
wysiwyg: clone(siteInfo.editors.wysiwyg.isActive)
},
locales: {
primary: clone(siteInfo.locales.primary),
active: clone(siteInfo.locales.active)
},
theme: {
...this.theme,
...clone(siteInfo.theme)

Loading…
Cancel
Save