Add support for async language pack loading (#7931)

#### What type of PR is this?

/area ui
/kind improvement
/milestone 2.22.x

#### What this PR does / why we need it:

This PR supports asynchronously loading the required language packs, which can significantly improve the initial page load speed.

before:

<img width="732" height="285" alt="image" src="https://github.com/user-attachments/assets/6d682010-751a-4c07-8651-8c6ade8a5f2e" />

after:

<img width="748" height="280" alt="image" src="https://github.com/user-attachments/assets/1c22d295-5ba9-423b-b941-3e7d536e1660" />


#### Does this PR introduce a user-facing change?

```release-note
Console 和 UC 支持异步加载所需语言包,提升首屏加载速度。
```
This commit is contained in:
Ryan Wang
2025-11-13 16:36:47 +08:00
committed by GitHub
parent dab1ceb537
commit ea315904a1
5 changed files with 108 additions and 110 deletions

View File

@@ -1,25 +1,22 @@
import { consoleApiClient } from "@halo-dev/api-client";
import { createPinia } from "pinia";
import type { DirectiveBinding } from "vue";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
// setup
import { getBrowserLanguage, i18n, setupI18n } from "@/locales";
import { setLanguage, setupI18n } from "@/locales";
import { setupApiClient } from "@/setup/setupApiClient";
import { setupComponents } from "@/setup/setupComponents";
import "@/setup/setupStyles";
// core modules
import { setupApiClient } from "@/setup/setupApiClient";
import { setupVueQuery } from "@/setup/setupVueQuery";
import { useRoleStore } from "@/stores/role";
import { getCookie } from "@/utils/cookie";
import {
setupCoreModules,
setupPluginModules,
} from "@console/setup/setupModules";
import { useThemeStore } from "@console/stores/theme";
import { consoleApiClient } from "@halo-dev/api-client";
import { stores, utils } from "@halo-dev/ui-shared";
import "core-js/es/object/has-own";
import { createPinia } from "pinia";
import type { DirectiveBinding } from "vue";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
@@ -74,13 +71,11 @@ async function initApp() {
const currentUserStore = stores.currentUser();
await currentUserStore.fetchCurrentUser();
// set locale
i18n.global.locale.value = getCookie("language") || getBrowserLanguage();
utils.date.setLocale(i18n.global.locale.value);
const globalInfoStore = stores.globalInfo();
await globalInfoStore.fetchGlobalInfo();
await setLanguage();
if (currentUserStore.isAnonymous) {
return;
}

View File

@@ -1,16 +1,17 @@
<script lang="ts" setup>
import HasPermission from "@/components/permission/HasPermission.vue";
import StickyBlock from "@/components/sticky-block/StickyBlock.vue";
import { setLanguage } from "@/locales";
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
import type { Setting } from "@halo-dev/api-client";
import { consoleApiClient } from "@halo-dev/api-client";
import { Toast, VButton, VLoading } from "@halo-dev/components";
import { stores, utils } from "@halo-dev/ui-shared";
import { stores } from "@halo-dev/ui-shared";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { computed, inject, ref, toRaw, type Ref } from "vue";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const { t } = useI18n();
const queryClient = useQueryClient();
const group = inject<Ref<string>>("activeTab", ref("basic"));
@@ -56,9 +57,8 @@ const handleSaveConfigMap = async (data: Record<string, unknown>) => {
if (group.value === "basic") {
const language = data.language;
locale.value = language as string;
document.cookie = `language=${language}; path=/; SameSite=Lax; Secure`;
utils.date.setLocale(locale.value);
await setLanguage(language as string);
}
Toast.success(t("core.common.toast.save_success"));

View File

@@ -1,42 +0,0 @@
<script lang="ts" setup>
import { getBrowserLanguage, i18n, locales } from "@/locales";
import { useLocalStorage } from "@vueuse/core";
import { watch } from "vue";
import MdiTranslate from "~icons/mdi/translate";
// setup locale
const currentLocale = useLocalStorage(
"locale",
getBrowserLanguage() || locales[0].code
);
watch(
() => currentLocale.value,
(value) => {
i18n.global.locale.value = value;
},
{
immediate: true,
}
);
</script>
<template>
<label
for="locale"
class="block flex-shrink-0 text-sm font-medium text-gray-600"
>
<MdiTranslate />
</label>
<select
id="locale"
v-model="currentLocale"
class="block appearance-none rounded-md border-0 py-1.5 pl-3 pr-10 text-sm text-gray-800 outline-none ring-1 ring-inset ring-gray-200 focus:!ring-1 focus:!ring-primary"
>
<template v-for="locale in locales">
<option v-if="locale.name" :key="locale.code" :value="locale.code">
{{ locale.name }}
</option>
</template>
</select>
</template>

View File

@@ -1,67 +1,115 @@
import { getCookie } from "@/utils/cookie";
import { utils } from "@halo-dev/ui-shared";
import type { App } from "vue";
import { createI18n } from "vue-i18n";
// @ts-ignore
import en from "./en.yaml";
// @ts-ignore
import es from "./es.yaml";
// @ts-ignore
import zhCN from "./zh-CN.yaml";
// @ts-ignore
import zhTW from "./zh-TW.yaml";
export const locales = [
interface LocaleConfig {
code: string[];
file: string;
}
export const SUPPORTED_LOCALES: LocaleConfig[] = [
{
code: "en",
package: en,
hidden: true,
code: ["en"],
file: "en.yaml",
},
{
name: "English",
code: "en-US",
package: en,
code: ["es"],
file: "es.yaml",
},
{
name: "Español",
code: "es",
package: es,
code: ["zh-CN", "zh"],
file: "zh-CN.yaml",
},
{
name: "简体中文",
code: "zh-CN",
package: zhCN,
},
{
code: "zh",
package: zhCN,
},
{
name: "正體中文",
code: "zh-TW",
package: zhTW,
code: ["zh-TW"],
file: "zh-TW.yaml",
},
];
const messages = locales.reduce((acc, cur) => {
acc[cur.code] = cur.package;
return acc;
}, {});
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>(
"./*.yaml",
{ eager: false }
);
const i18n = createI18n({
legacy: false,
locale: "en",
fallbackLocale: "en",
messages,
});
export function getBrowserLanguage(): string {
const browserLanguage = navigator.language;
const language = messages[browserLanguage]
? browserLanguage
: browserLanguage.split("-")[0];
return language in messages ? language : "zh-CN";
export function getEnvironmentLanguage(): string {
return getCookie("language") || navigator.language;
}
export function setupI18n(app: App) {
export function getLocaleDefinition(
language: string
): LocaleConfig | undefined {
const locale = SUPPORTED_LOCALES.find((locale) =>
locale.code.includes(language)
);
if (locale) {
return locale;
}
const code = language.split("-")[0];
return SUPPORTED_LOCALES.find((locale) => locale.code.includes(code));
}
export async function setLanguage(_language?: string): Promise<void> {
const language = _language || getEnvironmentLanguage();
if (!i18n.global.availableLocales.includes(language)) {
const locale = getLocaleDefinition(language);
if (locale) {
try {
const localeLoader = localeModules[`./${locale.file}`];
if (!localeLoader) {
throw new Error(`Locale file ${locale.file} not found`);
}
const messages = await localeLoader();
i18n.global.setLocaleMessage(language, messages.default || messages);
} catch (error) {
console.error(`Failed to load locale file for ${language}:`, error);
await loadFallbackLocale();
return;
}
} else {
console.warn(`Locale not found for ${language}, using fallback`);
await loadFallbackLocale();
return;
}
}
i18n.global.locale.value = language;
utils.date.setLocale(language);
}
async function loadFallbackLocale(): Promise<void> {
const fallback = i18n.global.fallbackLocale.value as string;
if (!i18n.global.availableLocales.includes(fallback)) {
const fallbackLocale = getLocaleDefinition(fallback);
if (fallbackLocale) {
try {
const localeLoader = localeModules[`./${fallbackLocale.file}`];
if (!localeLoader) {
throw new Error(
`Fallback locale file ${fallbackLocale.file} not found`
);
}
const messages = await localeLoader();
i18n.global.setLocaleMessage(fallback, messages.default || messages);
} catch (error) {
console.error(`Failed to load fallback locale file:`, error);
}
}
}
i18n.global.locale.value = fallback;
}
export function setupI18n(app: App): void {
app.use(i18n);
}

View File

@@ -1,10 +1,9 @@
import { getBrowserLanguage, i18n, setupI18n } from "@/locales";
import { setLanguage, setupI18n } from "@/locales";
import { setupApiClient } from "@/setup/setupApiClient";
import { setupComponents } from "@/setup/setupComponents";
import "@/setup/setupStyles";
import { setupVueQuery } from "@/setup/setupVueQuery";
import { useRoleStore } from "@/stores/role";
import { getCookie } from "@/utils/cookie";
import { consoleApiClient } from "@halo-dev/api-client";
import { stores, utils } from "@halo-dev/ui-shared";
import router from "@uc/router";
@@ -62,13 +61,11 @@ async function initApp() {
const currentUserStore = stores.currentUser();
await currentUserStore.fetchCurrentUser();
// set locale
i18n.global.locale.value = getCookie("language") || getBrowserLanguage();
utils.date.setLocale(i18n.global.locale.value);
const globalInfoStore = stores.globalInfo();
await globalInfoStore.fetchGlobalInfo();
await setLanguage();
if (currentUserStore.isAnonymous) {
return;
}