migrate i18n helper.js some code to typescript and migrate vue file to composition API and typescript

This commit is contained in:
MaysWind
2025-01-11 00:49:21 +08:00
parent 25c8b9baf8
commit 8da3d2aa35
30 changed files with 937 additions and 492 deletions
+80 -84
View File
@@ -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
View File
@@ -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 });
}
}
}
+1 -1
View File
@@ -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';
+5 -4
View File
@@ -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 -2
View File
@@ -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);
+5 -1
View File
@@ -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);
+5 -1
View File
@@ -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;
+9 -5
View File
@@ -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>({
+8 -4
View File
@@ -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() {
+8 -4
View File
@@ -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() {
+6 -2
View File
@@ -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>(() => {
+5
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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
View File
@@ -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();
}
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
export interface MapProvider {
getWebsite(): string;
asyncLoadAssets(language: string): Promise<unknown>;
asyncLoadAssets(language?: string): Promise<unknown>;
createMapInstance(): MapInstance | null;
}
+1 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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') {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+420
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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';
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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';