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 { createI18n } from 'vue-i18n';
import zh from './modules/zh';
import zhHant from './modules/zh-Hant'; type LocaleMessage = Record<string, unknown>;
import en from './modules/en'; type LocaleLoader = () => Promise<{ default: LocaleMessage }>;
import ptBr from './modules/pt-br';
import ja from './modules/ja'; const DEFAULT_LOCALE = 'en';
import ru from './modules/ru'; const STORAGE_KEY = 'lang';
import ms from './modules/ms';
import ko from './modules/ko'; const LOCALE_LOADERS: Record<string, LocaleLoader> = {
import tr from './modules/tr'; zh: () => import('./modules/zh'),
import esES from './modules/es-es'; '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({ const i18n = createI18n({
legacy: false, legacy: false,
missingWarn: false, missingWarn: false,
locale: localStorage.getItem('lang') || 'en', fallbackWarn: false,
fallbackLocale: 'en', locale: initialLocale,
fallbackLocale: DEFAULT_LOCALE,
globalInjection: true, globalInjection: true,
messages: { messages: initialMessages,
zh,
'zh-Hant': zhHant,
en,
'pt-BR': ptBr,
ja,
ru,
ms,
ko,
tr,
'es-ES': esES,
},
warnHtmlMessage: false, 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; export default i18n;

View file

@ -13,7 +13,7 @@ for (const path in styleModule) {
} }
import router from '@/routers/index'; import router from '@/routers/index';
import i18n from '@/lang/index'; import i18n, { ensureFallbackLocale, loadLocaleMessages } from '@/lang/index';
import pinia from '@/store/index'; import pinia from '@/store/index';
import SvgIcon from './components/svg-icon/svg-icon.vue'; import SvgIcon from './components/svg-icon/svg-icon.vue';
import Components from '@/components'; import Components from '@/components';
@ -24,10 +24,15 @@ import * as Icons from '@element-plus/icons-vue';
import directives from '@/directives/index'; import directives from '@/directives/index';
const bootstrap = async () => {
const currentLocale = i18n.global.locale.value;
await Promise.all([loadLocaleMessages(currentLocale), ensureFallbackLocale()]);
const app = createApp(App); const app = createApp(App);
app.component('SvgIcon', SvgIcon); app.component('SvgIcon', SvgIcon);
app.use(ElementPlus); app.use(ElementPlus);
app.use(Fit2CloudPlus, { locale: i18n.global.messages.value[localStorage.getItem('lang') || 'zh'] }); app.use(Fit2CloudPlus, { locale: i18n.global.getLocaleMessage(currentLocale) });
Object.keys(Icons).forEach((key) => { Object.keys(Icons).forEach((key) => {
app.component(key, Icons[key as keyof typeof Icons]); app.component(key, Icons[key as keyof typeof Icons]);
@ -40,3 +45,6 @@ app.use(Components);
app.use(directives); app.use(directives);
app.mount('#app'); app.mount('#app');
};
bootstrap();

View file

@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import piniaPersistConfig from '@/config/pinia-persist'; import piniaPersistConfig from '@/config/pinia-persist';
import { GlobalState, ThemeConfigProp } from '../interface'; import { GlobalState, ThemeConfigProp } from '../interface';
import { DeviceType } from '@/enums/app'; import { DeviceType } from '@/enums/app';
import i18n from '@/lang'; import i18n, { setActiveLocale } from '@/lang';
const GlobalStore = defineStore({ const GlobalStore = defineStore({
id: 'GlobalState', id: 'GlobalState',
@ -48,7 +48,7 @@ const GlobalStore = defineStore({
isMasterProductPro: false, isMasterProductPro: false,
isOffLine: false, isOffLine: false,
masterAlias: i18n.global.t('xpack.node.master'), masterAlias: '',
currentNode: 'local', currentNode: 'local',
currentNodeAddr: '', currentNodeAddr: '',
}), }),
@ -79,9 +79,10 @@ const GlobalStore = defineStore({
setCsrfToken(token: string) { setCsrfToken(token: string) {
this.csrfToken = token; this.csrfToken = token;
}, },
updateLanguage(language: any) { async updateLanguage(language: string) {
this.language = language; const activeLocale = await setActiveLocale(language);
localStorage.setItem('lang', language); this.language = activeLocale;
return activeLocale;
}, },
setThemeConfig(themeConfig: ThemeConfigProp) { setThemeConfig(themeConfig: ThemeConfigProp) {
this.themeConfig = themeConfig; this.themeConfig = themeConfig;

View file

@ -65,7 +65,7 @@
<el-dropdown-item command="ko">한국어</el-dropdown-item> <el-dropdown-item command="ko">한국어</el-dropdown-item>
<el-dropdown-item command="ru">Русский</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="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> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@ -199,7 +199,6 @@ const themeConfig = computed(() => globalStore.themeConfig);
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const menuStore = MenuStore(); const menuStore = MenuStore();
const tabsStore = TabsStore(); const tabsStore = TabsStore();
const usei18n = useI18n();
const errAuthInfo = ref(false); const errAuthInfo = ref(false);
const errCaptcha = ref(false); const errCaptcha = ref(false);
@ -276,32 +275,24 @@ const loading = ref<boolean>(false);
const mfaShow = ref<boolean>(false); const mfaShow = ref<boolean>(false);
const dropdownText = ref('中文(简体)'); const dropdownText = ref('中文(简体)');
function handleCommand(command: string) { const languageLabelMap: Record<string, string> = {
loginForm.language = command; zh: '中文(简体)',
usei18n.locale.value = command; en: 'English',
globalStore.updateLanguage(command); 'pt-BR': 'Português (Brasil)',
if (command === 'zh') { 'zh-Hant': '中文(繁體)',
dropdownText.value = '中文(简体)'; ko: '한국어',
} else if (command === 'en') { ja: '日本語',
dropdownText.value = 'English'; ru: 'Русский',
} else if (command === 'pt-BR') { ms: 'Bahasa Melayu',
dropdownText.value = 'Português (Brasil)'; tr: 'Turkish',
} else if (command === 'zh-Hant') { 'es-ES': 'España - Español',
dropdownText.value = '中文(繁體)'; };
} else if (command === 'ko') {
dropdownText.value = '한국어'; const handleCommand = async (command: string) => {
} else if (command === 'ja') { const activeLocale = await globalStore.updateLanguage(command);
dropdownText.value = '日本語'; loginForm.language = activeLocale;
} else if (command === 'ru') { dropdownText.value = languageLabelMap[activeLocale] || languageLabelMap.zh;
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 agreeWithLogin = () => { const agreeWithLogin = () => {
open.value = false; open.value = false;
@ -421,18 +412,16 @@ const getSetting = async () => {
try { try {
const res = await getLoginSetting(); const res = await getLoginSetting();
isDemo.value = res.data.isDemo; isDemo.value = res.data.isDemo;
loginForm.language = res.data.language; const language = res.data.language || loginForm.language;
handleCommand(loginForm.language); await handleCommand(language);
isIntl.value = res.data.isIntl; isIntl.value = res.data.isIntl;
isFxplay.value = res.data.isFxplay; isFxplay.value = res.data.isFxplay;
globalStore.isFxplay = isFxplay.value; globalStore.isFxplay = isFxplay.value;
globalStore.isOffLine = res.data.isOffLine; globalStore.isOffLine = res.data.isOffLine;
document.title = res.data.panelName; document.title = res.data.panelName;
i18n.locale.value = res.data.language;
i18n.warnHtmlMessage = false; i18n.warnHtmlMessage = false;
globalStore.setOpenMenuTabs(res.data.menuTabs === 'Enable'); globalStore.setOpenMenuTabs(res.data.menuTabs === 'Enable');
globalStore.updateLanguage(res.data.language);
globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName }); globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName });
} catch (error) {} } catch (error) {}
}; };

View file

@ -508,11 +508,10 @@ const onSave = async (key: string, val: any) => {
key: key, key: key,
value: val + '', value: val + '',
}; };
await updateSetting(param) try {
.then(() => { await updateSetting(param);
if (key === 'Language') { if (key === 'Language') {
i18n.global.locale.value = val; await globalStore.updateLanguage(val);
globalStore.updateLanguage(val);
location.reload(); location.reload();
} }
if (key === 'Theme') { if (key === 'Theme') {
@ -523,11 +522,11 @@ const onSave = async (key: string, val: any) => {
} }
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search(); search();
} catch (error) {
loading.value = false; loading.value = false;
}) return;
.catch(() => { }
loading.value = false; loading.value = false;
});
}; };
onMounted(() => { onMounted(() => {