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:
KOMATA 2025-10-27 17:49:52 +08:00 committed by GitHub
parent 1ef4803954
commit ff36b65af4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 152 additions and 95 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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;

View file

@ -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) {}
};

View file

@ -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(() => {