mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-19 22:09:03 +08:00
feat: Optimize i18n by dynamically loading module (#10759)
* feat: Optimize internationalization support, dynamically load language packs and update language settings * feat: Refactor i18n initialization to load initial messages dynamically
This commit is contained in:
parent
1ef4803954
commit
ff36b65af4
5 changed files with 152 additions and 95 deletions
|
|
@ -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<string, unknown>;
|
||||
type LocaleLoader = () => Promise<{ default: LocaleMessage }>;
|
||||
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
const STORAGE_KEY = 'lang';
|
||||
|
||||
const LOCALE_LOADERS: Record<string, LocaleLoader> = {
|
||||
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<string>();
|
||||
|
||||
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<Record<string, LocaleMessage>> => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
<el-dropdown-item command="ko">한국어</el-dropdown-item>
|
||||
<el-dropdown-item command="ru">Русский</el-dropdown-item>
|
||||
<el-dropdown-item command="ms">Bahasa Melayu</el-dropdown-item>
|
||||
<el-dropdown-item command="Tr">Turkish</el-dropdown-item>
|
||||
<el-dropdown-item command="tr">Turkish</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
|
@ -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<boolean>(false);
|
|||
const mfaShow = ref<boolean>(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<string, string> = {
|
||||
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) {}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue