migrate i18n helper.js some code to typescript and migrate vue file to composition API and typescript
This commit is contained in:
+80
-84
@@ -4,10 +4,10 @@
|
||||
</v-app>
|
||||
<v-snackbar class="cursor-pointer" color="notification-background" location="top"
|
||||
:multi-line="true" :timeout="-1" :close-on-content-click="true" v-model="showNotification">
|
||||
<v-tooltip activator="parent">{{ $t('Click to close') }}</v-tooltip>
|
||||
<v-tooltip activator="parent">{{ tt('Click to close') }}</v-tooltip>
|
||||
<div class="d-inline-flex">
|
||||
<img alt="logo" class="notification-logo" :src="ezBookkeepingLogoPath" />
|
||||
<span class="ml-2">{{ $t('global.app.title') }}</span>
|
||||
<img alt="logo" class="notification-logo" :src="APPLICATION_LOGO_PATH" />
|
||||
<span class="ml-2">{{ tt('global.app.title') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ currentNotificationContent }}
|
||||
@@ -15,11 +15,14 @@
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed, watch, onMounted } from 'vue';
|
||||
|
||||
import { useTheme } from 'vuetify';
|
||||
import { register } from 'register-service-worker';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useRootStore } from '@/stores/index.js';
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
@@ -33,90 +36,83 @@ import { initMapProvider } from '@/lib/map/index.ts';
|
||||
import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
|
||||
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showNotification: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
|
||||
ezBookkeepingLogoPath() {
|
||||
return APPLICATION_LOGO_PATH;
|
||||
},
|
||||
currentNotificationContent() {
|
||||
return this.rootStore.currentNotification;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentNotificationContent: function (newValue) {
|
||||
this.showNotification = !!newValue;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
const theme = useTheme();
|
||||
const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n();
|
||||
|
||||
if (self.settingsStore.appSettings.theme === ThemeType.Light) {
|
||||
theme.global.name.value = ThemeType.Light;
|
||||
} else if (self.settingsStore.appSettings.theme === ThemeType.Dark) {
|
||||
const theme = useTheme();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const tokensStore = useTokensStore();
|
||||
const exchangeRatesStore = useExchangeRatesStore();
|
||||
|
||||
const showNotification: Ref<boolean> = ref(false);
|
||||
|
||||
const currentNotificationContent = computed<string | null>(() => rootStore.currentNotification);
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageInfo = getCurrentLanguageInfo();
|
||||
initMapProvider(languageInfo?.alternativeLanguageTag);
|
||||
});
|
||||
});
|
||||
|
||||
watch(currentNotificationContent, (newValue) => {
|
||||
showNotification.value = !!newValue;
|
||||
});
|
||||
|
||||
if (settingsStore.appSettings.theme === ThemeType.Light) {
|
||||
theme.global.name.value = ThemeType.Light;
|
||||
} else if (settingsStore.appSettings.theme === ThemeType.Dark) {
|
||||
theme.global.name.value = ThemeType.Dark;
|
||||
} else {
|
||||
theme.global.name.value = getSystemTheme();
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
|
||||
if (settingsStore.appSettings.theme === 'auto') {
|
||||
if (e.matches) {
|
||||
theme.global.name.value = ThemeType.Dark;
|
||||
} else {
|
||||
theme.global.name.value = getSystemTheme();
|
||||
theme.global.name.value = ThemeType.Light;
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
|
||||
if (self.settingsStore.appSettings.theme === 'auto') {
|
||||
if (e.matches) {
|
||||
theme.global.name.value = ThemeType.Dark;
|
||||
} else {
|
||||
theme.global.name.value = ThemeType.Light;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
|
||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(self.userStore.currentUserExpenseAmountColor, self.userStore.currentUserIncomeAmountColor);
|
||||
|
||||
if (isUserLogined()) {
|
||||
if (!self.settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||
// refresh token if user is logined
|
||||
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||
if (response.user) {
|
||||
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
|
||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||
|
||||
if (response.notificationContent) {
|
||||
self.rootStore.setNotificationContent(response.notificationContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// auto refresh exchange rates data
|
||||
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isProduction()) {
|
||||
register('./sw.js', {
|
||||
registrationOptions: {
|
||||
scope: './'
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageInfo = this.$locale.getCurrentLanguageInfo();
|
||||
initMapProvider(languageInfo ? languageInfo.alternativeLanguageTag : null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let localeDefaultSettings = initLocale(userStore.currentUserLanguage, settingsStore.appSettings.timeZone);
|
||||
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor);
|
||||
|
||||
if (isUserLogined()) {
|
||||
if (!settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||
// refresh token if user is logined
|
||||
tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||
if (response.user) {
|
||||
localeDefaultSettings = setLanguage(response.user.language);
|
||||
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||
|
||||
if (response.notificationContent) {
|
||||
rootStore.setNotificationContent(response.notificationContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// auto refresh exchange rates data
|
||||
if (settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isProduction()) {
|
||||
register('./sw.js', {
|
||||
registrationOptions: {
|
||||
scope: './'
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+195
-194
@@ -4,11 +4,16 @@
|
||||
</f7-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed, watch, onMounted } from 'vue';
|
||||
|
||||
import type { Notification } from 'framework7/components/notification';
|
||||
import type { Actions, Dialog, Popover, Popup, Sheet } from 'framework7/types';
|
||||
import { f7ready } from 'framework7-vue';
|
||||
import routes from './router/mobile.js';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useRootStore } from '@/stores/index.js';
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
@@ -24,10 +29,23 @@ import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
|
||||
import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||
import { isModalShowing, setAppFontSize } from '@/lib/ui/mobile.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
const self = this;
|
||||
let darkMode = 'auto';
|
||||
const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const tokensStore = useTokensStore();
|
||||
const exchangeRatesStore = useExchangeRatesStore();
|
||||
|
||||
const f7params: Ref<object> = ref({
|
||||
name: 'ezBookkeeping',
|
||||
theme: 'ios',
|
||||
colors: {
|
||||
primary: '#c67e48'
|
||||
},
|
||||
routes: routes,
|
||||
darkMode: (() => {
|
||||
let darkMode: boolean | string = 'auto';
|
||||
|
||||
if (getTheme() === ThemeType.Light) {
|
||||
darkMode = false;
|
||||
@@ -35,207 +53,190 @@ export default {
|
||||
darkMode = true;
|
||||
}
|
||||
|
||||
return {
|
||||
notification: null,
|
||||
f7params: {
|
||||
name: 'ezBookkeeping',
|
||||
theme: 'ios',
|
||||
colors: {
|
||||
primary: '#c67e48'
|
||||
},
|
||||
routes: routes,
|
||||
darkMode: darkMode,
|
||||
touch: {
|
||||
disableContextMenu: true,
|
||||
tapHold: true
|
||||
},
|
||||
serviceWorker: {
|
||||
path: isProduction() ? './sw.js' : undefined,
|
||||
scope: './',
|
||||
},
|
||||
actions: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
dialog: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true
|
||||
},
|
||||
popover: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
popup: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true,
|
||||
swipeToClose: true
|
||||
},
|
||||
sheet: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
smartSelect: {
|
||||
routableModals: false
|
||||
},
|
||||
view: {
|
||||
animate: isEnableAnimate(),
|
||||
browserHistory: !self.isiOSHomeScreenMode(),
|
||||
browserHistoryInitialMatch: true,
|
||||
browserHistoryAnimate: false,
|
||||
iosSwipeBackAnimateShadow: false,
|
||||
mdSwipeBackAnimateShadow: false
|
||||
}
|
||||
},
|
||||
isDarkMode: undefined,
|
||||
hasPushPopupBackdrop: undefined,
|
||||
hasBackdrop: undefined
|
||||
}
|
||||
return darkMode;
|
||||
})(),
|
||||
touch: {
|
||||
disableContextMenu: true,
|
||||
tapHold: true
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
|
||||
currentNotificationContent() {
|
||||
return this.rootStore.currentNotification;
|
||||
}
|
||||
serviceWorker: {
|
||||
path: isProduction() ? './sw.js' : undefined,
|
||||
scope: './',
|
||||
},
|
||||
watch: {
|
||||
currentNotificationContent: function (newValue) {
|
||||
const self = this;
|
||||
actions: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
dialog: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true
|
||||
},
|
||||
popover: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
popup: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true,
|
||||
swipeToClose: true
|
||||
},
|
||||
sheet: {
|
||||
animate: isEnableAnimate(),
|
||||
backdrop: true,
|
||||
closeOnEscape: true
|
||||
},
|
||||
smartSelect: {
|
||||
routableModals: false
|
||||
},
|
||||
view: {
|
||||
animate: isEnableAnimate(),
|
||||
browserHistory: !isiOSHomeScreenMode(),
|
||||
browserHistoryInitialMatch: true,
|
||||
browserHistoryAnimate: false,
|
||||
iosSwipeBackAnimateShadow: false,
|
||||
mdSwipeBackAnimateShadow: false
|
||||
}
|
||||
});
|
||||
|
||||
if (self.notification) {
|
||||
self.notification.close();
|
||||
self.notification.destroy();
|
||||
self.notification = null;
|
||||
const notification: Ref<Notification.Notification | null> = ref(null);
|
||||
|
||||
const isDarkMode: Ref<boolean | undefined> = ref(undefined);
|
||||
const hasPushPopupBackdrop: Ref<boolean | undefined> = ref(undefined);
|
||||
const hasBackdrop: Ref<boolean | undefined> = ref(undefined);
|
||||
const currentNotificationContent = computed<string | null>(() => rootStore.currentNotification);
|
||||
|
||||
function isiOSHomeScreenMode(): boolean {
|
||||
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
|
||||
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setThemeColorMeta(darkMode: boolean | undefined): void {
|
||||
if (hasPushPopupBackdrop.value) {
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#000');
|
||||
return;
|
||||
}
|
||||
|
||||
if (darkMode) {
|
||||
if (hasBackdrop.value) {
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#0b0b0b');
|
||||
} else {
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#121212');
|
||||
}
|
||||
} else {
|
||||
if (hasBackdrop.value) {
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#949495');
|
||||
} else {
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#f6f6f8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onBackdropChanged(element: { push?: boolean, opened?: boolean }): void {
|
||||
if (element.push) {
|
||||
hasPushPopupBackdrop.value = element.opened;
|
||||
} else {
|
||||
hasBackdrop.value = element.opened;
|
||||
}
|
||||
|
||||
setThemeColorMeta(isDarkMode.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setAppFontSize(settingsStore.appSettings.fontSize);
|
||||
|
||||
f7ready((f7) => {
|
||||
isDarkMode.value = f7.darkMode;
|
||||
setThemeColorMeta(f7.darkMode);
|
||||
|
||||
f7.on('actionsOpen', (actions: Actions.Actions) => onBackdropChanged(actions));
|
||||
f7.on('actionsClose', (actions: Actions.Actions) => onBackdropChanged(actions));
|
||||
f7.on('dialogOpen', (dialog: Dialog.Dialog) => onBackdropChanged(dialog));
|
||||
f7.on('dialogClose', (dialog: Dialog.Dialog) => onBackdropChanged(dialog));
|
||||
f7.on('popoverOpen', (popover: Popover.Popover) => onBackdropChanged(popover));
|
||||
f7.on('popoverClose', (popover: Popover.Popover) => onBackdropChanged(popover));
|
||||
f7.on('popupOpen', (popup: Popup.Popup) => onBackdropChanged(popup));
|
||||
f7.on('popupClose', (popup: Popup.Popup) => onBackdropChanged(popup));
|
||||
f7.on('sheetOpen', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
|
||||
f7.on('sheetClose', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
|
||||
|
||||
f7.on('pageBeforeOut', () => {
|
||||
if (isModalShowing()) {
|
||||
f7.actions.close('.actions-modal.modal-in', false);
|
||||
f7.dialog.close('.dialog.modal-in', false);
|
||||
f7.popover.close('.popover.modal-in', false);
|
||||
f7.popup.close('.popup.modal-in', false);
|
||||
f7.sheet.close('.sheet-modal.modal-in', false);
|
||||
}
|
||||
});
|
||||
|
||||
if (newValue) {
|
||||
f7ready((f7) => {
|
||||
self.notification = f7.notification.create({
|
||||
icon: `<img alt="logo" src="${APPLICATION_LOGO_PATH}" />`,
|
||||
title: self.$t('global.app.title'),
|
||||
text: newValue,
|
||||
closeOnClick: true,
|
||||
on: {
|
||||
close() {
|
||||
self.rootStore.setNotificationContent(null);
|
||||
}
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
f7.on('darkModeChange', (darkMode) => {
|
||||
isDarkMode.value = darkMode;
|
||||
setThemeColorMeta(darkMode);
|
||||
});
|
||||
});
|
||||
|
||||
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
|
||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageInfo = getCurrentLanguageInfo();
|
||||
initMapProvider(languageInfo?.alternativeLanguageTag);
|
||||
});
|
||||
});
|
||||
|
||||
setExpenseAndIncomeAmountColor(self.userStore.currentUserExpenseAmountColor, self.userStore.currentUserIncomeAmountColor);
|
||||
|
||||
if (isUserLogined()) {
|
||||
if (!self.settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||
// refresh token if user is logined
|
||||
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||
if (response.user) {
|
||||
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
|
||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||
|
||||
if (response.notificationContent) {
|
||||
self.rootStore.setNotificationContent(response.notificationContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// auto refresh exchange rates data
|
||||
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setAppFontSize(this.settingsStore.appSettings.fontSize);
|
||||
watch(currentNotificationContent, (newValue) => {
|
||||
if (notification.value) {
|
||||
notification.value.close();
|
||||
notification.value.destroy();
|
||||
notification.value = null;
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
f7ready((f7) => {
|
||||
this.isDarkMode = f7.darkMode;
|
||||
this.setThemeColorMeta(f7.darkMode);
|
||||
|
||||
f7.on('actionsOpen', (event) => this.onBackdropChanged(event));
|
||||
f7.on('actionsClose', (event) => this.onBackdropChanged(event));
|
||||
f7.on('dialogOpen', (event) => this.onBackdropChanged(event));
|
||||
f7.on('dialogClose', (event) => this.onBackdropChanged(event));
|
||||
f7.on('popoverOpen', (event) => this.onBackdropChanged(event));
|
||||
f7.on('popoverClose', (event) => this.onBackdropChanged(event));
|
||||
f7.on('popupOpen', (event) => this.onBackdropChanged(event));
|
||||
f7.on('popupClose', (event) => this.onBackdropChanged(event));
|
||||
f7.on('sheetOpen', (event) => this.onBackdropChanged(event));
|
||||
f7.on('sheetClose', (event) => this.onBackdropChanged(event));
|
||||
|
||||
f7.on('pageBeforeOut', () => {
|
||||
if (isModalShowing()) {
|
||||
f7.actions.close('.actions-modal.modal-in', false);
|
||||
f7.dialog.close('.dialog.modal-in', false);
|
||||
f7.popover.close('.popover.modal-in', false);
|
||||
f7.popup.close('.popup.modal-in', false);
|
||||
f7.sheet.close('.sheet-modal.modal-in', false);
|
||||
notification.value = f7.notification.create({
|
||||
icon: `<img alt="logo" src="${APPLICATION_LOGO_PATH}" />`,
|
||||
title: tt('global.app.title'),
|
||||
text: newValue,
|
||||
closeOnClick: true,
|
||||
on: {
|
||||
close() {
|
||||
rootStore.setNotificationContent(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
f7.on('darkModeChange', (isDarkMode) => {
|
||||
this.isDarkMode = isDarkMode;
|
||||
this.setThemeColorMeta(isDarkMode);
|
||||
});
|
||||
let localeDefaultSettings = initLocale(userStore.currentUserLanguage, settingsStore.appSettings.timeZone);
|
||||
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor);
|
||||
|
||||
if (isUserLogined()) {
|
||||
if (!settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||
// refresh token if user is logined
|
||||
tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||
if (response.user) {
|
||||
localeDefaultSettings = setLanguage(response.user.language);
|
||||
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||
|
||||
if (response.notificationContent) {
|
||||
rootStore.setNotificationContent(response.notificationContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageInfo = this.$locale.getCurrentLanguageInfo();
|
||||
initMapProvider(languageInfo ? languageInfo.alternativeLanguageTag : null);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
isiOSHomeScreenMode() {
|
||||
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
|
||||
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onBackdropChanged(event) {
|
||||
if (event.push) {
|
||||
this.hasPushPopupBackdrop = event.opened;
|
||||
} else {
|
||||
this.hasBackdrop = event.opened;
|
||||
}
|
||||
|
||||
this.setThemeColorMeta(this.isDarkMode);
|
||||
},
|
||||
setThemeColorMeta(isDarkMode) {
|
||||
if (this.hasPushPopupBackdrop) {
|
||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#000');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDarkMode) {
|
||||
if (this.hasBackdrop) {
|
||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#0b0b0b');
|
||||
} else {
|
||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#121212');
|
||||
}
|
||||
} else {
|
||||
if (this.hasBackdrop) {
|
||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#949495');
|
||||
} else {
|
||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#f6f6f8');
|
||||
}
|
||||
}
|
||||
// auto refresh exchange rates data
|
||||
if (settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helper.js';
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { copyObjectTo } from '@/lib/common.ts';
|
||||
import type { MapInstance, MapPosition } from '@/lib/map/base.ts';
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<v-card-text v-if="textContent" class="pa-4 pb-6">{{ textContent }}</v-card-text>
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn :color="finalColor" @click="confirm">{{ $t('OK') }}</v-btn>
|
||||
<v-btn color="gray" @click="cancel">{{ tt('Cancel') }}</v-btn>
|
||||
<v-btn :color="finalColor" @click="confirm">{{ tt('OK') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -17,7 +17,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, watch } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helper.js';
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { isString } from '@/lib/common.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -46,7 +47,7 @@ function open(titleOrText: string, textOrOptions: string | Record<string, unknow
|
||||
|
||||
if (isString(textOrOptions)) { // second parameter is text
|
||||
titleContent.value = tt(titleOrText, options);
|
||||
textContent.value = tt(textOrOptions, options);
|
||||
textContent.value = tt(textOrOptions as string, options);
|
||||
} else { // second parameter is options
|
||||
const actualOptions = textOrOptions as Record<string, unknown>;
|
||||
titleContent.value = tt('global.app.title');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ messageContent }}
|
||||
|
||||
<template #actions>
|
||||
<v-btn color="primary" variant="text" @click="showState = false">{{ $t('Close') }}</v-btn>
|
||||
<v-btn color="primary" variant="text" @click="showState = false">{{ tt('Close') }}</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
@@ -11,8 +11,9 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, watch } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { isObject } from '@/lib/common.ts';
|
||||
import { useI18n } from '@/locales/helper.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<v-card class="pa-2 pa-sm-4 pa-md-4">
|
||||
<template #title>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<h4 class="text-h4">{{ $t('Use on Mobile Device') }}</h4>
|
||||
<h4 class="text-h4">{{ tt('Use on Mobile Device') }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div class="text-body-1 text-center text-wrap mt-4">{{ $t('You can scan the QR code below on your mobile device.') }}</div>
|
||||
<div class="text-body-1 text-center text-wrap mt-4">{{ tt('You can scan the QR code below on your mobile device.') }}</div>
|
||||
</template>
|
||||
<v-card-text class="mb-md-4">
|
||||
<v-row>
|
||||
@@ -20,8 +20,8 @@
|
||||
</v-card-text>
|
||||
<v-card-text class="overflow-y-visible">
|
||||
<div class="w-100 d-flex justify-center gap-4">
|
||||
<v-btn :href="mobileVersionPath">{{$t('Switch to Mobile Version') }}</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" @click="showState = false">{{ $t('Close') }}</v-btn>
|
||||
<v-btn :href="mobileVersionPath">{{ tt('Switch to Mobile Version') }}</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" @click="showState = false">{{ tt('Close') }}</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -31,6 +31,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { getMobileUrlQrCodePath } from '@/lib/qrcode.ts';
|
||||
import { getMobileVersionPath } from '@/lib/version.ts';
|
||||
|
||||
@@ -42,6 +44,8 @@ const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const mobileUrlQrCodePath = getMobileUrlQrCodePath();
|
||||
const mobileVersionPath = getMobileVersionPath();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="swipe-handler"></div>
|
||||
<div class="left"></div>
|
||||
<div class="right">
|
||||
<f7-link sheet-close :text="$t('Done')"></f7-link>
|
||||
<f7-link sheet-close :text="tt('Done')"></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-page-content>
|
||||
@@ -31,6 +31,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import type { ColorValue, ColorInfo } from '@/core/color.ts';
|
||||
import { arrayContainsFieldValue } from '@/lib/common.ts';
|
||||
import { getColorsInRows } from '@/lib/color.ts';
|
||||
@@ -48,6 +50,8 @@ const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const currentValue: Ref<ColorValue> = ref(props.modelValue);
|
||||
const itemPerRow: Ref<number> = ref(props.columnCount || 7);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="swipe-handler"></div>
|
||||
<div class="left"></div>
|
||||
<div class="right">
|
||||
<f7-link sheet-close :text="$t('Done')"></f7-link>
|
||||
<f7-link sheet-close :text="tt('Done')"></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-page-content>
|
||||
@@ -31,6 +31,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import type { IconInfo, IconInfoWithId } from '@/core/icon.ts';
|
||||
import { arrayContainsFieldValue } from '@/lib/common.ts';
|
||||
import { getIconsInRows } from '@/lib/icon.ts';
|
||||
@@ -49,6 +51,8 @@ const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const currentValue: Ref<string> = ref(props.modelValue);
|
||||
const itemPerRow: Ref<number> = ref(props.columnCount || 7);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
<textarea class="information-content full-line" readonly="readonly" :rows="rowCount" :value="information"></textarea>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link @click="close" :text="$t('Close')"></f7-link>
|
||||
<f7-link @click="close" :text="tt('Close')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
@@ -27,6 +27,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef, watch, onMounted, onUpdated } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { ClipboardHolder } from '@/lib/clipboard.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -43,6 +45,8 @@ const emit = defineEmits<{
|
||||
(e: 'info:copied'): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const iconCopyToClipboard = useTemplateRef('copyToClipboardIcon');
|
||||
|
||||
let clipboardHolder: ClipboardHolder | null = null;
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
<div class="swipe-handler"></div>
|
||||
<div class="left"></div>
|
||||
<div class="right">
|
||||
<f7-link :text="$t('Done')" @click="save"></f7-link>
|
||||
<f7-link :text="tt('Done')" @click="save"></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-page-content class="no-margin-vertical no-padding-vertical">
|
||||
<map-view ref="map" height="400px" :geo-location="geoLocation">
|
||||
<template #error-title="{ mapSupported, mapDependencyLoaded }">
|
||||
<div class="display-flex padding justify-content-space-between align-items-center">
|
||||
<div class="ebk-sheet-title" v-if="!mapSupported"><b>{{ $t('Unsupported Map Provider') }}</b></div>
|
||||
<div class="ebk-sheet-title" v-else-if="!mapDependencyLoaded"><b>{{ $t('Cannot Initialize Map') }}</b></div>
|
||||
<div class="ebk-sheet-title" v-if="!mapSupported"><b>{{ tt('Unsupported Map Provider') }}</b></div>
|
||||
<div class="ebk-sheet-title" v-else-if="!mapDependencyLoaded"><b>{{ tt('Cannot Initialize Map') }}</b></div>
|
||||
<div class="ebk-sheet-title" v-else></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #error-content>
|
||||
<div class="padding-horizontal padding-bottom">
|
||||
<p class="no-margin">{{ $t('Please refresh the page and try again. If the error persists, ensure that the server\'s map settings are correctly configured.') }}</p>
|
||||
<p class="no-margin">{{ tt('Please refresh the page and try again. If the error persists, ensure that the server\'s map settings are correctly configured.') }}</p>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link @click="close" :text="$t('Close')"></f7-link>
|
||||
<f7-link @click="close" :text="tt('Close')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -34,6 +34,8 @@
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import MapView from '@/components/common/MapView.vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import type { MapPosition } from '@/lib/map/base.ts';
|
||||
|
||||
type MapViewType = InstanceType<typeof MapView>;
|
||||
@@ -48,6 +50,8 @@ const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const map = useTemplateRef<MapViewType>('map');
|
||||
|
||||
const geoLocation = computed<MapPosition | undefined>({
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
floating-label
|
||||
clear-button
|
||||
class="no-margin no-padding-bottom"
|
||||
:label="$t('Passcode')"
|
||||
:placeholder="$t('Passcode')"
|
||||
:label="tt('Passcode')"
|
||||
:placeholder="tt('Passcode')"
|
||||
v-model:value="currentPasscode"
|
||||
@keyup.enter="confirm()"
|
||||
></f7-list-input>
|
||||
</f7-list>
|
||||
<f7-button large fill
|
||||
:class="{ 'disabled': !currentPasscode || confirmDisabled }"
|
||||
:text="$t('Continue')"
|
||||
:text="tt('Continue')"
|
||||
@click="confirm">
|
||||
</f7-button>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link :class="{ 'disabled': cancelDisabled }" @click="cancel" :text="$t('Cancel')"></f7-link>
|
||||
<f7-link :class="{ 'disabled': cancelDisabled }" @click="cancel" :text="tt('Cancel')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
@@ -39,6 +39,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
title?: string
|
||||
@@ -54,6 +56,8 @@ const emit = defineEmits<{
|
||||
(e: 'passcode:confirm', value: string): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const currentPasscode: Ref<string> = ref('');
|
||||
|
||||
function confirm() {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
clear-button
|
||||
class="no-margin no-padding-bottom"
|
||||
:class="color ? 'color-' + color : ''"
|
||||
:label="$t('Current Password')"
|
||||
:placeholder="$t('Current Password')"
|
||||
:label="tt('Current Password')"
|
||||
:placeholder="tt('Current Password')"
|
||||
v-model:value="currentPassword"
|
||||
@keyup.enter="confirm()"
|
||||
></f7-list-input>
|
||||
@@ -26,11 +26,11 @@
|
||||
<f7-button large fill
|
||||
:class="{ 'disabled': !currentPassword || confirmDisabled }"
|
||||
:color="color || 'primary'"
|
||||
:text="$t('Continue')"
|
||||
:text="tt('Continue')"
|
||||
@click="confirm">
|
||||
</f7-button>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link :class="{ 'disabled': cancelDisabled }" @click="cancel" :text="$t('Cancel')"></f7-link>
|
||||
<f7-link :class="{ 'disabled': cancelDisabled }" @click="cancel" :text="tt('Cancel')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
@@ -40,6 +40,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
title?: string
|
||||
@@ -56,6 +58,8 @@ const emit = defineEmits<{
|
||||
(e: 'password:confirm', value: string): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const currentPassword: Ref<string> = ref('');
|
||||
|
||||
function confirm() {
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
</f7-list>
|
||||
<f7-button large fill
|
||||
:class="{ 'disabled': !currentPinCodeValid || confirmDisabled }"
|
||||
:text="$t('Continue')"
|
||||
:text="tt('Continue')"
|
||||
@click="confirm">
|
||||
</f7-button>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link @click="cancel" :text="$t('Cancel')"></f7-link>
|
||||
<f7-link @click="cancel" :text="tt('Cancel')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
@@ -29,6 +29,8 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
title?: string
|
||||
@@ -44,6 +46,8 @@ const emit = defineEmits<{
|
||||
(e: 'pincode:confirm', value: string): void
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const currentPinCode: Ref<string> = ref('');
|
||||
|
||||
const currentPinCodeValid = computed<boolean>(() => {
|
||||
|
||||
@@ -62,6 +62,11 @@ export interface LocalizedRecentMonthDateRange {
|
||||
readonly displayName: string;
|
||||
}
|
||||
|
||||
export interface LocalizedMeridiemIndicator {
|
||||
readonly values: string[];
|
||||
readonly displayValues: string[];
|
||||
}
|
||||
|
||||
export class YearUnixTime implements UnixTimeRange {
|
||||
public readonly year: number;
|
||||
public readonly minUnixTime: number;
|
||||
|
||||
+1
-1
@@ -71,8 +71,8 @@ import draggable from 'vuedraggable';
|
||||
import router from '@/router/desktop.js';
|
||||
|
||||
import { getVersion, getBuildTime } from '@/lib/version.ts';
|
||||
import { getI18nOptions } from '@/locales/helpers.ts';
|
||||
import {
|
||||
getI18nOptions,
|
||||
translateIf,
|
||||
translateError,
|
||||
i18nFunctions
|
||||
|
||||
Vendored
+12
@@ -10,3 +10,15 @@ interface Window {
|
||||
[key: string]: string | number | boolean | undefined | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
browserLanguage?: string;
|
||||
}
|
||||
|
||||
declare module "framework7/components/notification" {
|
||||
export namespace Notification {
|
||||
export interface Notification {
|
||||
destroy(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export class AmapMapProvider implements MapProvider {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public asyncLoadAssets(language: string): Promise<unknown> {
|
||||
public asyncLoadAssets(language?: string): Promise<unknown> {
|
||||
if (AmapMapProvider.AMap) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class BaiduMapProvider implements MapProvider {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public asyncLoadAssets(language: string): Promise<unknown> {
|
||||
public asyncLoadAssets(language?: string): Promise<unknown> {
|
||||
if (BaiduMapProvider.BMap) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export interface MapProvider {
|
||||
getWebsite(): string;
|
||||
asyncLoadAssets(language: string): Promise<unknown>;
|
||||
asyncLoadAssets(language?: string): Promise<unknown>;
|
||||
createMapInstance(): MapInstance | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class GoogleMapProvider implements MapProvider {
|
||||
return 'https://maps.google.com';
|
||||
}
|
||||
|
||||
public asyncLoadAssets(language: string): Promise<unknown> {
|
||||
public asyncLoadAssets(language?: string): Promise<unknown> {
|
||||
if (GoogleMapProvider.GoogleMap) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AmapMapProvider } from './amap.ts';
|
||||
|
||||
let mapProvider: MapProvider | null = null;
|
||||
|
||||
export function initMapProvider(language: string): void {
|
||||
export function initMapProvider(language?: string): void {
|
||||
const mapProviderType = getMapProvider();
|
||||
|
||||
if (LEAFLET_TILE_SOURCES[mapProviderType] || mapProviderType === 'custom') {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class LeafletMapProvider implements MapProvider {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public asyncLoadAssets(language: string): Promise<unknown> {
|
||||
public asyncLoadAssets(language?: string): Promise<unknown> {
|
||||
return Promise.all([
|
||||
import('leaflet/dist/leaflet.css'),
|
||||
import('leaflet/dist/leaflet-src.esm.js').then(leaflet => LeafletMapProvider.Leaflet = leaflet)
|
||||
|
||||
+1
-1
@@ -540,7 +540,7 @@ export default {
|
||||
|
||||
return url;
|
||||
},
|
||||
generateGoogleMapJavascriptUrl: (language: string, callbackFnName: string): string => {
|
||||
generateGoogleMapJavascriptUrl: (language: string | undefined, callbackFnName: string): string => {
|
||||
let url = `${GOOGLE_MAP_JAVASCRIPT_URL}?key=${getGoogleMapAPIKey()}&libraries=core,marker&callback=${callbackFnName}`;
|
||||
|
||||
if (language) {
|
||||
|
||||
+5
-29
@@ -1,7 +1,7 @@
|
||||
import { useI18n as useVueI18n } from 'vue-i18n';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { defaultLanguage, allLanguages } from '@/locales/index.ts';
|
||||
import { DEFAULT_LANGUAGE, allLanguages } from '@/locales/index.ts';
|
||||
|
||||
import { Month, WeekDay, MeridiemIndicator, LongDateFormat, ShortDateFormat, LongTimeFormat, ShortTimeFormat, DateRange, LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE } from '@/core/datetime.ts';
|
||||
import { TimezoneTypeForStatistics } from '@/core/timezone.ts';
|
||||
@@ -113,13 +113,13 @@ function getLanguageInfo(locale) {
|
||||
|
||||
function getDefaultLanguage() {
|
||||
if (!window || !window.navigator) {
|
||||
return defaultLanguage;
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
let browserLocale = window.navigator.browserLanguage || window.navigator.language;
|
||||
|
||||
if (!browserLocale) {
|
||||
return defaultLanguage;
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLocale]) {
|
||||
@@ -153,7 +153,7 @@ function getDefaultLanguage() {
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLocale]) {
|
||||
return defaultLanguage;
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
return browserLocale;
|
||||
@@ -1270,7 +1270,7 @@ function getAllSupportedImportFileTypes(i18nGlobal, translateFn) {
|
||||
document.anchor = document.anchor.toLowerCase().replace(/ /g, '-');
|
||||
}
|
||||
|
||||
if (document.language === defaultLanguage) {
|
||||
if (document.language === DEFAULT_LANGUAGE) {
|
||||
document.language = '';
|
||||
}
|
||||
} else {
|
||||
@@ -1541,29 +1541,6 @@ function initLocale(i18nGlobal, lastUserLanguage, timezone) {
|
||||
return localeDefaultSettings;
|
||||
}
|
||||
|
||||
export function getI18nOptions() {
|
||||
return {
|
||||
legacy: true,
|
||||
locale: defaultLanguage,
|
||||
fallbackLocale: defaultLanguage,
|
||||
formatFallbackMessages: true,
|
||||
messages: (function () {
|
||||
const messages = {};
|
||||
|
||||
for (let locale in allLanguages) {
|
||||
if (!Object.prototype.hasOwnProperty.call(allLanguages, locale)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lang = allLanguages[locale];
|
||||
messages[locale] = lang.content;
|
||||
}
|
||||
|
||||
return messages;
|
||||
})()
|
||||
};
|
||||
}
|
||||
|
||||
export function translateIf(text, isTranslate, translateFn) {
|
||||
if (isTranslate) {
|
||||
return translateFn(text);
|
||||
@@ -1588,7 +1565,6 @@ export function i18nFunctions(i18nGlobal) {
|
||||
return {
|
||||
getAllLanguageInfoArray: (includeSystemDefault) => getAllLanguageInfoArray(i18nGlobal.t, includeSystemDefault),
|
||||
getLanguageInfo: getLanguageInfo,
|
||||
getDefaultLanguage: getDefaultLanguage,
|
||||
getCurrentLanguageTag: () => getCurrentLanguageTag(i18nGlobal),
|
||||
getCurrentLanguageInfo: () => getCurrentLanguageInfo(i18nGlobal),
|
||||
getCurrentLanguageDisplayName: () => getCurrentLanguageDisplayName(i18nGlobal),
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import { useI18n as useVueI18n } from 'vue-i18n';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { type LanguageInfo, allLanguages, DEFAULT_LANGUAGE } from '@/locales/index.ts';
|
||||
|
||||
import {
|
||||
type LocalizedMeridiemIndicator,
|
||||
Month,
|
||||
WeekDay,
|
||||
MeridiemIndicator
|
||||
} from '@/core/datetime.ts';
|
||||
|
||||
import type { LocaleDefaultSettings } from '@/core/setting.ts';
|
||||
import type { ErrorResponse } from '@/core/api.ts';
|
||||
|
||||
import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts';
|
||||
|
||||
import {
|
||||
isString,
|
||||
isObject
|
||||
} from '@/lib/common.ts';
|
||||
|
||||
import {
|
||||
isPM,
|
||||
getTimezoneOffset
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
import logger from '@/lib/logger.ts';
|
||||
import services from '@/lib/services.ts';
|
||||
|
||||
export interface LocalizedErrorParameter {
|
||||
key: string;
|
||||
localized: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface LocalizedError {
|
||||
message: string;
|
||||
parameters?: LocalizedErrorParameter[];
|
||||
}
|
||||
|
||||
export function getI18nOptions(): object {
|
||||
return {
|
||||
legacy: true,
|
||||
locale: DEFAULT_LANGUAGE,
|
||||
fallbackLocale: DEFAULT_LANGUAGE,
|
||||
formatFallbackMessages: true,
|
||||
messages: (function () {
|
||||
const messages: Record<string, object> = {};
|
||||
|
||||
for (const languageKey in allLanguages) {
|
||||
if (!Object.prototype.hasOwnProperty.call(allLanguages, languageKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const languageInfo = allLanguages[languageKey];
|
||||
messages[languageKey] = languageInfo.content;
|
||||
}
|
||||
|
||||
return messages;
|
||||
})()
|
||||
};
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const { t, locale } = useVueI18n();
|
||||
|
||||
// private functions
|
||||
function getLanguageInfo(languageKey: string): LanguageInfo | undefined {
|
||||
return allLanguages[languageKey];
|
||||
}
|
||||
|
||||
function getDefaultLanguage(): string {
|
||||
if (!window || !window.navigator) {
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
let browserLanguage = window.navigator.browserLanguage || window.navigator.language;
|
||||
|
||||
if (!browserLanguage) {
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLanguage]) {
|
||||
const languageKey = getLanguageKeyFromLanguageAlias(browserLanguage);
|
||||
|
||||
if (languageKey) {
|
||||
browserLanguage = languageKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLanguage] && browserLanguage.split('-').length > 1) { // maybe language-script-region
|
||||
const languageTagParts = browserLanguage.split('-');
|
||||
browserLanguage = languageTagParts[0] + '-' + languageTagParts[1];
|
||||
|
||||
if (!allLanguages[browserLanguage]) {
|
||||
const languageKey = getLanguageKeyFromLanguageAlias(browserLanguage);
|
||||
|
||||
if (languageKey) {
|
||||
browserLanguage = languageKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLanguage]) {
|
||||
browserLanguage = languageTagParts[0];
|
||||
const languageKey = getLanguageKeyFromLanguageAlias(browserLanguage);
|
||||
|
||||
if (languageKey) {
|
||||
browserLanguage = languageKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allLanguages[browserLanguage]) {
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
return browserLanguage;
|
||||
}
|
||||
|
||||
function getLanguageKeyFromLanguageAlias(alias: string): string | null {
|
||||
for (const languageKey in allLanguages) {
|
||||
if (!Object.prototype.hasOwnProperty.call(allLanguages, languageKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (languageKey.toLowerCase() === alias.toLowerCase()) {
|
||||
return languageKey;
|
||||
}
|
||||
|
||||
const langInfo = allLanguages[languageKey];
|
||||
const aliases = langInfo.aliases;
|
||||
|
||||
if (!aliases || aliases.length < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < aliases.length; i++) {
|
||||
if (aliases[i].toLowerCase() === alias.toLowerCase()) {
|
||||
return languageKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLocalizedError(error: ErrorResponse): LocalizedError {
|
||||
if (error.errorCode === KnownErrorCode.ApiNotFound && SPECIFIED_API_NOT_FOUND_ERRORS[error.path]) {
|
||||
return {
|
||||
message: `${SPECIFIED_API_NOT_FOUND_ERRORS[error.path].message}`
|
||||
};
|
||||
}
|
||||
|
||||
if (error.errorCode !== KnownErrorCode.ValidatorError) {
|
||||
return {
|
||||
message: `error.${error.errorMessage}`
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < PARAMETERIZED_ERRORS.length; i++) {
|
||||
const errorInfo = PARAMETERIZED_ERRORS[i];
|
||||
const matches = error.errorMessage.match(errorInfo.regex);
|
||||
|
||||
if (matches && matches.length === errorInfo.parameters.length + 1) {
|
||||
return {
|
||||
message: `parameterizedError.${errorInfo.localeKey}`,
|
||||
parameters: errorInfo.parameters.map((param, index) => {
|
||||
return {
|
||||
key: param.field,
|
||||
localized: param.localized,
|
||||
value: matches[index + 1]
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: `error.${error.errorMessage}`
|
||||
};
|
||||
}
|
||||
|
||||
function getLocalizedErrorParameters(parameters?: LocalizedErrorParameter[]): Record<string, string> {
|
||||
const localizedParameters: Record<string, string> = {};
|
||||
|
||||
if (parameters) {
|
||||
for (let i = 0; i < parameters.length; i++) {
|
||||
const parameter = parameters[i];
|
||||
|
||||
if (parameter.localized) {
|
||||
localizedParameters[parameter.key] = t(`parameter.${parameter.value}`);
|
||||
} else {
|
||||
localizedParameters[parameter.key] = parameter.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return localizedParameters;
|
||||
}
|
||||
|
||||
function getAllMonthNames(type: string): string[] {
|
||||
const ret = [];
|
||||
const allMonths = Month.values();
|
||||
|
||||
for (let i = 0; i < allMonths.length; i++) {
|
||||
const month = allMonths[i];
|
||||
ret.push(t(`datetime.${month.name}.${type}`));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getAllWeekdayNames(type: string): string[] {
|
||||
const ret = [];
|
||||
const allWeekDays = WeekDay.values();
|
||||
|
||||
for (let i = 0; i < allWeekDays.length; i++) {
|
||||
const weekDay = allWeekDays[i];
|
||||
ret.push(t(`datetime.${weekDay.name}.${type}`));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// public functions
|
||||
function translateIf(text: string, isTranslate: boolean): string {
|
||||
if (isTranslate) {
|
||||
return t(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function translateError(message: string | { error: ErrorResponse }): string {
|
||||
let finalMessage = '';
|
||||
let parameters = {};
|
||||
|
||||
if (isObject(message) && isObject((message as { error: ErrorResponse }).error)) {
|
||||
const localizedError = getLocalizedError((message as { error: ErrorResponse }).error);
|
||||
finalMessage = localizedError.message;
|
||||
parameters = getLocalizedErrorParameters(localizedError.parameters);
|
||||
} else if (isString(message)) {
|
||||
finalMessage = message as string;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
return t(finalMessage, parameters);
|
||||
}
|
||||
|
||||
function getCurrentLanguageTag(): string {
|
||||
return locale.value;
|
||||
}
|
||||
|
||||
function getCurrentLanguageInfo(): LanguageInfo {
|
||||
const languageInfo = getLanguageInfo(locale.value);
|
||||
|
||||
if (languageInfo) {
|
||||
return languageInfo;
|
||||
}
|
||||
|
||||
return getLanguageInfo(getDefaultLanguage()) as LanguageInfo;
|
||||
}
|
||||
|
||||
function getCurrentLanguageDisplayName() {
|
||||
const currentLanguageInfo = getCurrentLanguageInfo();
|
||||
return currentLanguageInfo.displayName;
|
||||
}
|
||||
|
||||
function getDefaultCurrency(): string {
|
||||
return t('default.currency');
|
||||
}
|
||||
|
||||
function getDefaultFirstDayOfWeek(): string {
|
||||
return t('default.firstDayOfWeek');
|
||||
}
|
||||
|
||||
function getCurrencyName(currencyCode: string): string {
|
||||
return t(`currency.name.${currencyCode}`);
|
||||
}
|
||||
|
||||
function getAllMeridiemIndicators(): LocalizedMeridiemIndicator {
|
||||
const allMeridiemIndicators = MeridiemIndicator.values();
|
||||
const meridiemIndicatorNames = [];
|
||||
const localizedMeridiemIndicatorNames = [];
|
||||
|
||||
for (let i = 0; i < allMeridiemIndicators.length; i++) {
|
||||
const indicator = allMeridiemIndicators[i];
|
||||
|
||||
meridiemIndicatorNames.push(indicator.name);
|
||||
localizedMeridiemIndicatorNames.push(t(`datetime.${indicator.name}.content`));
|
||||
}
|
||||
|
||||
return {
|
||||
values: meridiemIndicatorNames,
|
||||
displayValues: localizedMeridiemIndicatorNames
|
||||
};
|
||||
}
|
||||
|
||||
function getAllLongMonthNames(): string[] {
|
||||
return getAllMonthNames('long');
|
||||
}
|
||||
|
||||
function getAllShortMonthNames(): string[] {
|
||||
return getAllMonthNames('short');
|
||||
}
|
||||
|
||||
function getAllLongWeekdayNames(): string[] {
|
||||
return getAllWeekdayNames('long');
|
||||
}
|
||||
|
||||
function getAllShortWeekdayNames(): string[] {
|
||||
return getAllWeekdayNames('short');
|
||||
}
|
||||
|
||||
function getAllMinWeekdayNames(): string[] {
|
||||
return getAllWeekdayNames('min');
|
||||
}
|
||||
|
||||
function setLanguage(languageKey: string | null, force?: boolean): LocaleDefaultSettings | null {
|
||||
if (!languageKey) {
|
||||
languageKey = getDefaultLanguage();
|
||||
logger.info(`No specified language, use browser default language ${languageKey}`);
|
||||
}
|
||||
|
||||
if (!getLanguageInfo(languageKey)) {
|
||||
languageKey = getDefaultLanguage();
|
||||
logger.warn(`Not found language ${languageKey}, use browser default language ${languageKey}`);
|
||||
}
|
||||
|
||||
if (!force && locale.value === languageKey) {
|
||||
logger.info(`Current locale is already ${languageKey}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info(`Apply current language to ${languageKey}`);
|
||||
|
||||
locale.value = languageKey;
|
||||
moment.updateLocale(languageKey, {
|
||||
months : getAllLongMonthNames(),
|
||||
monthsShort : getAllShortMonthNames(),
|
||||
weekdays : getAllLongWeekdayNames(),
|
||||
weekdaysShort : getAllShortWeekdayNames(),
|
||||
weekdaysMin : getAllMinWeekdayNames(),
|
||||
meridiem: function (hours) {
|
||||
if (isPM(hours)) {
|
||||
return t(`datetime.${MeridiemIndicator.PM.name}.content`);
|
||||
} else {
|
||||
return t(`datetime.${MeridiemIndicator.AM.name}.content`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
services.setLocale(languageKey);
|
||||
document.querySelector('html')?.setAttribute('lang', languageKey);
|
||||
|
||||
const defaultCurrency = getDefaultCurrency();
|
||||
const defaultFirstDayOfWeekName = getDefaultFirstDayOfWeek();
|
||||
let defaultFirstDayOfWeek = WeekDay.DefaultFirstDay.type;
|
||||
|
||||
if (WeekDay.parse(defaultFirstDayOfWeekName)) {
|
||||
defaultFirstDayOfWeek = WeekDay.parse(defaultFirstDayOfWeekName).type;
|
||||
}
|
||||
|
||||
return {
|
||||
currency: defaultCurrency,
|
||||
firstDayOfWeek: defaultFirstDayOfWeek
|
||||
};
|
||||
}
|
||||
|
||||
function setTimeZone(timezone: string): void {
|
||||
if (timezone) {
|
||||
moment.tz.setDefault(timezone);
|
||||
} else {
|
||||
moment.tz.setDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function initLocale(lastUserLanguage: string, timezone: string): LocaleDefaultSettings | null {
|
||||
let localeDefaultSettings: LocaleDefaultSettings | null = null;
|
||||
|
||||
if (lastUserLanguage && getLanguageInfo(lastUserLanguage)) {
|
||||
logger.info(`Last user language is ${lastUserLanguage}`);
|
||||
localeDefaultSettings = setLanguage(lastUserLanguage, true);
|
||||
} else {
|
||||
localeDefaultSettings = setLanguage(null, true);
|
||||
}
|
||||
|
||||
if (timezone) {
|
||||
logger.info(`Current timezone is ${timezone}`);
|
||||
setTimeZone(timezone);
|
||||
} else {
|
||||
logger.info(`No timezone is set, use browser default ${getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`);
|
||||
}
|
||||
|
||||
return localeDefaultSettings;
|
||||
}
|
||||
|
||||
return {
|
||||
tt: t,
|
||||
ti: translateIf,
|
||||
te: translateError,
|
||||
getCurrentLanguageTag,
|
||||
getCurrentLanguageInfo,
|
||||
getCurrentLanguageDisplayName,
|
||||
getDefaultCurrency,
|
||||
getDefaultFirstDayOfWeek,
|
||||
getCurrencyName,
|
||||
getAllMeridiemIndicators,
|
||||
getAllLongMonthNames,
|
||||
getAllShortMonthNames,
|
||||
getAllLongWeekdayNames,
|
||||
getAllShortWeekdayNames,
|
||||
getAllMinWeekdayNames,
|
||||
setLanguage,
|
||||
setTimeZone,
|
||||
initLocale
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import en from './en.json';
|
||||
import vi from './vi.json';
|
||||
import zhHans from './zh_Hans.json';
|
||||
|
||||
interface LanguageInfo {
|
||||
export interface LanguageInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
alternativeLanguageTag: string;
|
||||
@@ -10,7 +10,7 @@ interface LanguageInfo {
|
||||
content: object;
|
||||
}
|
||||
|
||||
export const defaultLanguage: string = 'en';
|
||||
export const DEFAULT_LANGUAGE: string = 'en';
|
||||
|
||||
// To add new languages, please refer to https://ezbookkeeping.mayswind.net/translating
|
||||
export const allLanguages: Record<string, LanguageInfo> = {
|
||||
|
||||
+1
-1
@@ -80,8 +80,8 @@ import VueDatePicker from '@vuepic/vue-datepicker';
|
||||
import '@vuepic/vue-datepicker/dist/main.css';
|
||||
|
||||
import { getVersion, getBuildTime } from '@/lib/version.ts';
|
||||
import { getI18nOptions } from '@/locales/helpers.ts';
|
||||
import {
|
||||
getI18nOptions,
|
||||
translateIf,
|
||||
i18nFunctions
|
||||
} from '@/locales/helper.js';
|
||||
|
||||
@@ -144,7 +144,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
appSettings.value = getApplicationSettings();
|
||||
}
|
||||
|
||||
function updateLocalizedDefaultSettings(newLocaleDefaultSettings: LocaleDefaultSettings) {
|
||||
function updateLocalizedDefaultSettings(newLocaleDefaultSettings: LocaleDefaultSettings | null) {
|
||||
if (!newLocaleDefaultSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
+144
-143
@@ -5,9 +5,9 @@
|
||||
<div class="nav-header">
|
||||
<router-link to="/" class="app-logo d-flex align-center gap-x-3 app-title-wrapper">
|
||||
<div class="d-flex">
|
||||
<img alt="logo" class="main-logo" :src="ezBookkeepingLogoPath" />
|
||||
<img alt="logo" class="main-logo" :src="APPLICATION_LOGO_PATH" />
|
||||
</div>
|
||||
<h1 class="font-weight-medium text-xl">{{ $t('global.app.title') }}</h1>
|
||||
<h1 class="font-weight-medium text-xl">{{ tt('global.app.title') }}</h1>
|
||||
</router-link>
|
||||
</div>
|
||||
<perfect-scrollbar
|
||||
@@ -18,82 +18,82 @@
|
||||
<li class="nav-link home-link">
|
||||
<router-link to="/">
|
||||
<v-icon class="nav-item-icon" :icon="icons.overview"/>
|
||||
<span class="nav-item-title">{{ $t('Overview') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Overview') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<span class="title-text">{{ $t('Transaction Data') }}</span>
|
||||
<span class="title-text">{{ tt('Transaction Data') }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/transaction/list?dateType=7">
|
||||
<v-icon class="nav-item-icon" :icon="icons.transactions"/>
|
||||
<span class="nav-item-title">{{ $t('Transaction Details') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Transaction Details') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/statistics/transaction">
|
||||
<v-icon class="nav-item-icon" :icon="icons.statistics"/>
|
||||
<span class="nav-item-title">{{ $t('Statistics & Analysis') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Statistics & Analysis') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<span class="title-text">{{ $t('Basis Data') }}</span>
|
||||
<span class="title-text">{{ tt('Basis Data') }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/account/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.accounts"/>
|
||||
<span class="nav-item-title">{{ $t('Accounts') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Accounts') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/category/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.categories"/>
|
||||
<span class="nav-item-title">{{ $t('Transaction Categories') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Transaction Categories') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/tag/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.tags"/>
|
||||
<span class="nav-item-title">{{ $t('Transaction Tags') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Transaction Tags') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/template/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.templates"/>
|
||||
<span class="nav-item-title">{{ $t('Transaction Templates') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Transaction Templates') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link" v-if="isUserScheduledTransactionEnabled">
|
||||
<li class="nav-link" v-if="isUserScheduledTransactionEnabled()">
|
||||
<router-link to="/schedule/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.scheduledTransactions"/>
|
||||
<span class="nav-item-title">{{ $t('Scheduled Transactions') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Scheduled Transactions') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<span class="title-text">{{ $t('Miscellaneous') }}</span>
|
||||
<span class="title-text">{{ tt('Miscellaneous') }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/exchange_rates">
|
||||
<v-icon class="nav-item-icon" :icon="icons.exchangeRates"/>
|
||||
<span class="nav-item-title">{{ $t('Exchange Rates Data') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Exchange Rates Data') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<a href="javascript:void(0);" @click="showMobileQrCode = true">
|
||||
<v-icon class="nav-item-icon" :icon="icons.mobile"/>
|
||||
<span class="nav-item-title">{{ $t('Use on Mobile Device') }}</span>
|
||||
<span class="nav-item-title">{{ tt('Use on Mobile Device') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/about">
|
||||
<v-icon class="nav-item-icon" :icon="icons.about"/>
|
||||
<span class="nav-item-title">{{ $t('About') }}</span>
|
||||
<span class="nav-item-title">{{ tt('About') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</perfect-scrollbar>
|
||||
@@ -109,14 +109,14 @@
|
||||
</v-btn>
|
||||
<div class="app-logo d-flex align-center gap-x-3 app-title-wrapper" v-if="mdAndDown">
|
||||
<div class="d-flex">
|
||||
<img alt="logo" class="main-logo" :src="ezBookkeepingLogoPath" />
|
||||
<img alt="logo" class="main-logo" :src="APPLICATION_LOGO_PATH" />
|
||||
</div>
|
||||
<h1 class="font-weight-medium text-xl">{{ $t('global.app.title') }}</h1>
|
||||
<h1 class="font-weight-medium text-xl">{{ tt('global.app.title') }}</h1>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="text" class="me-2"
|
||||
:icon="true" @click="(theme === 'light' ? theme = 'dark' : (theme === 'dark' ? theme = 'auto' : theme = 'light'))">
|
||||
<v-icon :icon="(theme === 'light' ? icons.themeLight : (theme === 'dark' ? icons.themeDark : icons.themeAuto))" size="24" />
|
||||
:icon="true" @click="(currentTheme === 'light' ? currentTheme = 'dark' : (currentTheme === 'dark' ? currentTheme = 'auto' : currentTheme = 'light'))">
|
||||
<v-icon :icon="(currentTheme === 'light' ? icons.themeLight : (currentTheme === 'dark' ? icons.themeDark : icons.themeAuto))" size="24" />
|
||||
</v-btn>
|
||||
<v-avatar class="cursor-pointer" variant="tonal"
|
||||
:color="currentUserAvatar ? 'rgba(0,0,0,0)' : 'primary'">
|
||||
@@ -152,19 +152,19 @@
|
||||
</v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="icons.profile"
|
||||
:title="$t('User Settings')"
|
||||
:title="tt('User Settings')"
|
||||
to="/user/settings"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.settings"
|
||||
:title="$t('Application Settings')"
|
||||
:title="tt('Application Settings')"
|
||||
to="/app/settings"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="icons.lock"
|
||||
:title="$t('Lock Application')"
|
||||
:title="tt('Lock Application')"
|
||||
v-if="isEnableApplicationLock"
|
||||
@click="lock"></v-list-item>
|
||||
<v-list-item :disabled="logouting"
|
||||
:prepend-icon="icons.logout"
|
||||
:title="$t('Log Out')"
|
||||
:title="tt('Log Out')"
|
||||
@click="logout"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -191,12 +191,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { type Ref, ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useDisplay } from 'vuetify';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useRootStore } from '@/stores/index.js';
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
@@ -206,6 +211,8 @@ import { ThemeType } from '@/core/theme.ts';
|
||||
import { isUserScheduledTransactionEnabled } from '@/lib/server_settings.ts';
|
||||
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
import {
|
||||
mdiMenu,
|
||||
mdiHomeOutline,
|
||||
@@ -229,124 +236,118 @@ import {
|
||||
mdiLogout
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
logouting: false,
|
||||
isVerticalNavScrolled: false,
|
||||
showVerticalOverlayMenu: false,
|
||||
showLoading: false,
|
||||
showMobileQrCode: false,
|
||||
icons: {
|
||||
menu: mdiMenu,
|
||||
overview: mdiHomeOutline,
|
||||
transactions: mdiListBoxOutline,
|
||||
accounts: mdiCreditCardOutline,
|
||||
categories: mdiViewDashboardOutline,
|
||||
tags: mdiTagOutline,
|
||||
templates: mdiClipboardTextOutline,
|
||||
scheduledTransactions: mdiClipboardTextClockOutline,
|
||||
statistics: mdiChartPieOutline,
|
||||
exchangeRates: mdiSwapHorizontal,
|
||||
settings: mdiCogOutline,
|
||||
mobile: mdiCellphone,
|
||||
about: mdiInformationOutline,
|
||||
themeAuto: mdiThemeLightDark,
|
||||
themeLight: mdiWeatherSunny,
|
||||
themeDark: mdiWeatherNight,
|
||||
user: mdiAccount,
|
||||
profile: mdiAccountCogOutline,
|
||||
lock: mdiLockOutline,
|
||||
logout: mdiLogout
|
||||
const display = useDisplay();
|
||||
const theme = useTheme();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { tt, initLocale } = useI18n();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const icons = {
|
||||
menu: mdiMenu,
|
||||
overview: mdiHomeOutline,
|
||||
transactions: mdiListBoxOutline,
|
||||
accounts: mdiCreditCardOutline,
|
||||
categories: mdiViewDashboardOutline,
|
||||
tags: mdiTagOutline,
|
||||
templates: mdiClipboardTextOutline,
|
||||
scheduledTransactions: mdiClipboardTextClockOutline,
|
||||
statistics: mdiChartPieOutline,
|
||||
exchangeRates: mdiSwapHorizontal,
|
||||
settings: mdiCogOutline,
|
||||
mobile: mdiCellphone,
|
||||
about: mdiInformationOutline,
|
||||
themeAuto: mdiThemeLightDark,
|
||||
themeLight: mdiWeatherSunny,
|
||||
themeDark: mdiWeatherNight,
|
||||
user: mdiAccount,
|
||||
profile: mdiAccountCogOutline,
|
||||
lock: mdiLockOutline,
|
||||
logout: mdiLogout
|
||||
};
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const logouting: Ref<boolean> = ref(false);
|
||||
const isVerticalNavScrolled: Ref<boolean> = ref(false);
|
||||
const showVerticalOverlayMenu: Ref<boolean> = ref(false);
|
||||
const showLoading: Ref<boolean> = ref(false);
|
||||
const showMobileQrCode: Ref<boolean> = ref(false);
|
||||
|
||||
const mdAndDown = computed<boolean>(() => {
|
||||
return display.mdAndDown.value;
|
||||
});
|
||||
|
||||
const currentRoutePath = computed<string>(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
const currentNickName = computed<string>(() => {
|
||||
return userStore.currentUserNickname || tt('User');
|
||||
});
|
||||
|
||||
const currentUserAvatar = computed<string | null>(() => {
|
||||
return userStore.getUserAvatarUrl(userStore.currentUserBasicInfo, true);
|
||||
});
|
||||
|
||||
const currentTheme = computed<string>({
|
||||
get: () => {
|
||||
return settingsStore.appSettings.theme;
|
||||
},
|
||||
set: (value: string) => {
|
||||
if (value !== settingsStore.appSettings.theme) {
|
||||
settingsStore.setTheme(value);
|
||||
|
||||
if (value === ThemeType.Light || value === ThemeType.Dark) {
|
||||
theme.global.name.value = value;
|
||||
} else {
|
||||
theme.global.name.value = getSystemTheme();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUserStore),
|
||||
ezBookkeepingLogoPath() {
|
||||
return APPLICATION_LOGO_PATH;
|
||||
},
|
||||
mdAndDown() {
|
||||
return this.display.mdAndDown.value;
|
||||
},
|
||||
currentRoutePath() {
|
||||
const route = useRoute();
|
||||
return route.path;
|
||||
},
|
||||
currentNickName() {
|
||||
return this.userStore.currentUserNickname || this.$t('User');
|
||||
},
|
||||
currentUserAvatar() {
|
||||
return this.userStore.getUserAvatarUrl(this.userStore.currentUserBasicInfo, true);
|
||||
},
|
||||
theme: {
|
||||
get: function () {
|
||||
return this.settingsStore.appSettings.theme;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value !== this.settingsStore.appSettings.theme) {
|
||||
this.settingsStore.setTheme(value);
|
||||
|
||||
if (value === ThemeType.Light || value === ThemeType.Dark) {
|
||||
this.globalTheme.global.name.value = value;
|
||||
} else {
|
||||
this.globalTheme.global.name.value = getSystemTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isUserScheduledTransactionEnabled() {
|
||||
return isUserScheduledTransactionEnabled();
|
||||
},
|
||||
isEnableApplicationLock() {
|
||||
return this.settingsStore.appSettings.applicationLock;
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const display = useDisplay();
|
||||
const theme = useTheme();
|
||||
|
||||
return {
|
||||
display: display,
|
||||
globalTheme: theme
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleNavScroll(e) {
|
||||
this.isVerticalNavScrolled = e.target.scrollTop > 0;
|
||||
},
|
||||
lock() {
|
||||
this.rootStore.lock();
|
||||
this.$router.replace('/unlock');
|
||||
},
|
||||
logout() {
|
||||
const self = this;
|
||||
|
||||
self.logouting = true;
|
||||
self.showLoading = true;
|
||||
|
||||
self.rootStore.logout().then(() => {
|
||||
self.logouting = false;
|
||||
self.showLoading = false;
|
||||
|
||||
self.settingsStore.clearAppSettings();
|
||||
|
||||
const localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
|
||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(self.userStore.currentUserExpenseAmountColor, self.userStore.currentUserIncomeAmountColor);
|
||||
|
||||
this.$router.replace('/login');
|
||||
}).catch(error => {
|
||||
self.logouting = false;
|
||||
self.showLoading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const isEnableApplicationLock = computed<boolean>(() => {
|
||||
return settingsStore.appSettings.applicationLock;
|
||||
});
|
||||
|
||||
function handleNavScroll(e: Event) {
|
||||
isVerticalNavScrolled.value = (e.target as HTMLElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
function lock() {
|
||||
rootStore.lock();
|
||||
router.replace('/unlock');
|
||||
}
|
||||
|
||||
function logout() {
|
||||
logouting.value = true;
|
||||
showLoading.value = true;
|
||||
|
||||
rootStore.logout().then(() => {
|
||||
logouting.value = false;
|
||||
showLoading.value = false;
|
||||
|
||||
settingsStore.clearAppSettings();
|
||||
|
||||
const localeDefaultSettings = initLocale(userStore.currentUserLanguage, settingsStore.appSettings.timeZone);
|
||||
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
|
||||
setExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor);
|
||||
|
||||
router.replace('/login');
|
||||
}).catch(error => {
|
||||
logouting.value = false;
|
||||
showLoading.value = false;
|
||||
|
||||
if (!error.processed && snackbar.value) {
|
||||
snackbar.value.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ export default defineConfig(() => {
|
||||
return 'common';
|
||||
} else if (/[\\/]src[\\/]components[\\/](base|common)[\\/]/i.test(id)) {
|
||||
return 'common';
|
||||
} else if (/[\\/]src[\\/]locales[\\/]helper\.(js|ts)/i.test(id)) {
|
||||
} else if (/[\\/]src[\\/]locales[\\/]helpers\.(js|ts)/i.test(id)) {
|
||||
return 'common';
|
||||
} else if (/[\\/]src[\\/]locales[\\/]/i.test(id)) {
|
||||
return 'locales';
|
||||
|
||||
Reference in New Issue
Block a user