diff --git a/frontend/src/lang/index.ts b/frontend/src/lang/index.ts index 786045021..9902a50ed 100644 --- a/frontend/src/lang/index.ts +++ b/frontend/src/lang/index.ts @@ -1,34 +1,94 @@ import { createI18n } from 'vue-i18n'; -import zh from './modules/zh'; -import zhHant from './modules/zh-Hant'; -import en from './modules/en'; -import ptBr from './modules/pt-br'; -import ja from './modules/ja'; -import ru from './modules/ru'; -import ms from './modules/ms'; -import ko from './modules/ko'; -import tr from './modules/tr'; -import esES from './modules/es-es'; + +type LocaleMessage = Record; +type LocaleLoader = () => Promise<{ default: LocaleMessage }>; + +const DEFAULT_LOCALE = 'en'; +const STORAGE_KEY = 'lang'; + +const LOCALE_LOADERS: Record = { + zh: () => import('./modules/zh'), + 'zh-Hant': () => import('./modules/zh-Hant'), + en: () => import('./modules/en'), + 'pt-BR': () => import('./modules/pt-br'), + ja: () => import('./modules/ja'), + ru: () => import('./modules/ru'), + ms: () => import('./modules/ms'), + ko: () => import('./modules/ko'), + tr: () => import('./modules/tr'), + 'es-ES': () => import('./modules/es-es'), +}; + +const getStoredLocale = () => { + if (typeof window === 'undefined') return DEFAULT_LOCALE; + return localStorage.getItem(STORAGE_KEY) || DEFAULT_LOCALE; +}; + +const initialLocale = getStoredLocale(); + +const loadedLocales = new Set(); + +export const loadLocaleMessages = async (locale: string) => { + const targetLocale = LOCALE_LOADERS[locale] ? locale : DEFAULT_LOCALE; + if (loadedLocales.has(targetLocale)) { + return targetLocale; + } + const loader = LOCALE_LOADERS[targetLocale]; + if (!loader) { + return targetLocale; + } + const messagesModule = await loader(); + const messages = messagesModule.default || {}; + if (!i18n) { + return targetLocale; + } + i18n.global.setLocaleMessage(targetLocale, messages); + loadedLocales.add(targetLocale); + return targetLocale; +}; + +const getInitialMessages = async (): Promise> => { + const loader = LOCALE_LOADERS[initialLocale]; + if (!loader) { + return { [initialLocale]: {} }; + } + try { + const messagesModule = await loader(); + const messages = messagesModule.default || {}; + loadedLocales.add(initialLocale); + return { [initialLocale]: messages }; + } catch { + return { [initialLocale]: {} }; + } +}; + +const initialMessages = await getInitialMessages(); const i18n = createI18n({ legacy: false, missingWarn: false, - locale: localStorage.getItem('lang') || 'en', - fallbackLocale: 'en', + fallbackWarn: false, + locale: initialLocale, + fallbackLocale: DEFAULT_LOCALE, globalInjection: true, - messages: { - zh, - 'zh-Hant': zhHant, - en, - 'pt-BR': ptBr, - ja, - ru, - ms, - ko, - tr, - 'es-ES': esES, - }, + messages: initialMessages, warnHtmlMessage: false, }); +export const ensureFallbackLocale = async () => { + const fallback = i18n.global.fallbackLocale.value || DEFAULT_LOCALE; + if (typeof fallback === 'string') { + await loadLocaleMessages(fallback); + } +}; + +export const setActiveLocale = async (locale: string) => { + const loaded = await loadLocaleMessages(locale); + i18n.global.locale.value = loaded; + if (typeof window !== 'undefined') { + localStorage.setItem(STORAGE_KEY, loaded); + } + return loaded; +}; + export default i18n; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 017f3bba6..baf5916d8 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -13,7 +13,7 @@ for (const path in styleModule) { } import router from '@/routers/index'; -import i18n from '@/lang/index'; +import i18n, { ensureFallbackLocale, loadLocaleMessages } from '@/lang/index'; import pinia from '@/store/index'; import SvgIcon from './components/svg-icon/svg-icon.vue'; import Components from '@/components'; @@ -24,19 +24,27 @@ import * as Icons from '@element-plus/icons-vue'; import directives from '@/directives/index'; -const app = createApp(App); -app.component('SvgIcon', SvgIcon); -app.use(ElementPlus); -app.use(Fit2CloudPlus, { locale: i18n.global.messages.value[localStorage.getItem('lang') || 'zh'] }); +const bootstrap = async () => { + const currentLocale = i18n.global.locale.value; -Object.keys(Icons).forEach((key) => { - app.component(key, Icons[key as keyof typeof Icons]); -}); + await Promise.all([loadLocaleMessages(currentLocale), ensureFallbackLocale()]); -app.use(router); -app.use(i18n); -app.use(pinia); -app.use(Components); -app.use(directives); + const app = createApp(App); + app.component('SvgIcon', SvgIcon); + app.use(ElementPlus); + app.use(Fit2CloudPlus, { locale: i18n.global.getLocaleMessage(currentLocale) }); -app.mount('#app'); + Object.keys(Icons).forEach((key) => { + app.component(key, Icons[key as keyof typeof Icons]); + }); + + app.use(router); + app.use(i18n); + app.use(pinia); + app.use(Components); + app.use(directives); + + app.mount('#app'); +}; + +bootstrap(); diff --git a/frontend/src/store/modules/global.ts b/frontend/src/store/modules/global.ts index b0017f28b..de0781a3f 100644 --- a/frontend/src/store/modules/global.ts +++ b/frontend/src/store/modules/global.ts @@ -2,7 +2,7 @@ import { defineStore } from 'pinia'; import piniaPersistConfig from '@/config/pinia-persist'; import { GlobalState, ThemeConfigProp } from '../interface'; import { DeviceType } from '@/enums/app'; -import i18n from '@/lang'; +import i18n, { setActiveLocale } from '@/lang'; const GlobalStore = defineStore({ id: 'GlobalState', @@ -48,7 +48,7 @@ const GlobalStore = defineStore({ isMasterProductPro: false, isOffLine: false, - masterAlias: i18n.global.t('xpack.node.master'), + masterAlias: '', currentNode: 'local', currentNodeAddr: '', }), @@ -79,9 +79,10 @@ const GlobalStore = defineStore({ setCsrfToken(token: string) { this.csrfToken = token; }, - updateLanguage(language: any) { - this.language = language; - localStorage.setItem('lang', language); + async updateLanguage(language: string) { + const activeLocale = await setActiveLocale(language); + this.language = activeLocale; + return activeLocale; }, setThemeConfig(themeConfig: ThemeConfigProp) { this.themeConfig = themeConfig; diff --git a/frontend/src/views/login/components/login-form.vue b/frontend/src/views/login/components/login-form.vue index a3e424e1c..f37ea7b35 100644 --- a/frontend/src/views/login/components/login-form.vue +++ b/frontend/src/views/login/components/login-form.vue @@ -65,7 +65,7 @@ 한국어 Русский Bahasa Melayu - Turkish + Turkish @@ -199,7 +199,6 @@ const themeConfig = computed(() => globalStore.themeConfig); const globalStore = GlobalStore(); const menuStore = MenuStore(); const tabsStore = TabsStore(); -const usei18n = useI18n(); const errAuthInfo = ref(false); const errCaptcha = ref(false); @@ -276,32 +275,24 @@ const loading = ref(false); const mfaShow = ref(false); const dropdownText = ref('中文(简体)'); -function handleCommand(command: string) { - loginForm.language = command; - usei18n.locale.value = command; - globalStore.updateLanguage(command); - if (command === 'zh') { - dropdownText.value = '中文(简体)'; - } else if (command === 'en') { - dropdownText.value = 'English'; - } else if (command === 'pt-BR') { - dropdownText.value = 'Português (Brasil)'; - } else if (command === 'zh-Hant') { - dropdownText.value = '中文(繁體)'; - } else if (command === 'ko') { - dropdownText.value = '한국어'; - } else if (command === 'ja') { - dropdownText.value = '日本語'; - } else if (command === 'ru') { - dropdownText.value = 'Русский'; - } else if (command === 'ms') { - dropdownText.value = 'Bahasa Melayu'; - } else if (command === 'tr') { - dropdownText.value = 'Turkish'; - } else if (command === 'es-ES') { - dropdownText.value = 'España - Español'; - } -} +const languageLabelMap: Record = { + zh: '中文(简体)', + en: 'English', + 'pt-BR': 'Português (Brasil)', + 'zh-Hant': '中文(繁體)', + ko: '한국어', + ja: '日本語', + ru: 'Русский', + ms: 'Bahasa Melayu', + tr: 'Turkish', + 'es-ES': 'España - Español', +}; + +const handleCommand = async (command: string) => { + const activeLocale = await globalStore.updateLanguage(command); + loginForm.language = activeLocale; + dropdownText.value = languageLabelMap[activeLocale] || languageLabelMap.zh; +}; const agreeWithLogin = () => { open.value = false; @@ -421,18 +412,16 @@ const getSetting = async () => { try { const res = await getLoginSetting(); isDemo.value = res.data.isDemo; - loginForm.language = res.data.language; - handleCommand(loginForm.language); + const language = res.data.language || loginForm.language; + await handleCommand(language); isIntl.value = res.data.isIntl; isFxplay.value = res.data.isFxplay; globalStore.isFxplay = isFxplay.value; globalStore.isOffLine = res.data.isOffLine; document.title = res.data.panelName; - i18n.locale.value = res.data.language; i18n.warnHtmlMessage = false; globalStore.setOpenMenuTabs(res.data.menuTabs === 'Enable'); - globalStore.updateLanguage(res.data.language); globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName }); } catch (error) {} }; diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index 4ac2622ca..6a5211c89 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -508,26 +508,25 @@ const onSave = async (key: string, val: any) => { key: key, value: val + '', }; - await updateSetting(param) - .then(() => { - if (key === 'Language') { - i18n.global.locale.value = val; - globalStore.updateLanguage(val); - location.reload(); - } - if (key === 'Theme') { - handleThemeChange(val); - } - if (key === 'MenuTabs') { - globalStore.setOpenMenuTabs(val === 'Enable'); - } - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - search(); - loading.value = false; - }) - .catch(() => { - loading.value = false; - }); + try { + await updateSetting(param); + if (key === 'Language') { + await globalStore.updateLanguage(val); + location.reload(); + } + if (key === 'Theme') { + handleThemeChange(val); + } + if (key === 'MenuTabs') { + globalStore.setOpenMenuTabs(val === 'Enable'); + } + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + search(); + } catch (error) { + loading.value = false; + return; + } + loading.value = false; }; onMounted(() => {