mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-18 12:05:53 +00:00
fix: split locale chunks by removing eager i18n glob
The eager `import.meta.glob` was statically pulling all 13 locale JSON files into the main bundle, defeating the sibling lazy glob and emitting INEFFECTIVE_DYNAMIC_IMPORT warnings. Statically import only the en-US fallback, lazy-load the rest, and await `readyI18n()` in each entry before mount so the first paint still uses the active locale.
This commit is contained in:
parent
19d50bd16c
commit
79a9be7b22
9 changed files with 51 additions and 76 deletions
|
|
@ -4,7 +4,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||||
|
|
||||||
import { setupAxios } from '@/api/axios-init.js';
|
import { setupAxios } from '@/api/axios-init.js';
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import ApiDocsPage from '@/pages/api-docs/ApiDocsPage.vue';
|
import ApiDocsPage from '@/pages/api-docs/ApiDocsPage.vue';
|
||||||
|
|
||||||
|
|
@ -16,4 +16,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(ApiDocsPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(ApiDocsPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||||
|
|
||||||
import { setupAxios } from '@/api/axios-init.js';
|
import { setupAxios } from '@/api/axios-init.js';
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import InboundsPage from '@/pages/inbounds/InboundsPage.vue';
|
import InboundsPage from '@/pages/inbounds/InboundsPage.vue';
|
||||||
|
|
||||||
|
|
@ -16,4 +16,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(InboundsPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(InboundsPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
// Importing useTheme triggers the boot side-effect that applies the
|
// Importing useTheme triggers the boot side-effect that applies the
|
||||||
// stored theme to <body>/<html> before Vue mounts.
|
// stored theme to <body>/<html> before Vue mounts.
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import IndexPage from '@/pages/index/IndexPage.vue';
|
import IndexPage from '@/pages/index/IndexPage.vue';
|
||||||
|
|
||||||
|
|
@ -18,4 +18,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(IndexPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(IndexPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,18 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
// Importing this module triggers the boot side-effect that applies the
|
// Importing this module triggers the boot side-effect that applies the
|
||||||
// stored theme to <body>/<html> before Vue renders anything.
|
// stored theme to <body>/<html> before Vue renders anything.
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import LoginPage from '@/pages/login/LoginPage.vue';
|
import LoginPage from '@/pages/login/LoginPage.vue';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
applyDocumentTitle();
|
applyDocumentTitle();
|
||||||
|
|
||||||
// Toasts attach to a #message div the page provides — keeps theme
|
|
||||||
// styling in sync with the rest of the panel.
|
|
||||||
const messageContainer = document.getElementById('message');
|
const messageContainer = document.getElementById('message');
|
||||||
if (messageContainer) {
|
if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(LoginPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(LoginPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||||
|
|
||||||
import { setupAxios } from '@/api/axios-init.js';
|
import { setupAxios } from '@/api/axios-init.js';
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import NodesPage from '@/pages/nodes/NodesPage.vue';
|
import NodesPage from '@/pages/nodes/NodesPage.vue';
|
||||||
|
|
||||||
|
|
@ -16,4 +16,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(NodesPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(NodesPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
// Importing useTheme triggers the boot side-effect that applies the
|
// Importing useTheme triggers the boot side-effect that applies the
|
||||||
// stored theme to <body>/<html> before Vue mounts.
|
// stored theme to <body>/<html> before Vue mounts.
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import SettingsPage from '@/pages/settings/SettingsPage.vue';
|
import SettingsPage from '@/pages/settings/SettingsPage.vue';
|
||||||
|
|
||||||
|
|
@ -18,4 +18,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(SettingsPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(SettingsPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||||
// with the parsed traffic/quota/expiry view-model and the rendered
|
// with the parsed traffic/quota/expiry view-model and the rendered
|
||||||
// share links — the SPA reads those at mount.
|
// share links — the SPA reads those at mount.
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import SubPage from '@/pages/sub/SubPage.vue';
|
import SubPage from '@/pages/sub/SubPage.vue';
|
||||||
|
|
||||||
const messageContainer = document.getElementById('message');
|
const messageContainer = document.getElementById('message');
|
||||||
|
|
@ -15,4 +15,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(SubPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(SubPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||||
|
|
||||||
import { setupAxios } from '@/api/axios-init.js';
|
import { setupAxios } from '@/api/axios-init.js';
|
||||||
import '@/composables/useTheme.js';
|
import '@/composables/useTheme.js';
|
||||||
import { i18n } from '@/i18n/index.js';
|
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import XrayPage from '@/pages/xray/XrayPage.vue';
|
import XrayPage from '@/pages/xray/XrayPage.vue';
|
||||||
|
|
||||||
|
|
@ -16,4 +16,6 @@ if (messageContainer) {
|
||||||
message.config({ getContainer: () => messageContainer });
|
message.config({ getContainer: () => messageContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(XrayPage).use(Antd).use(i18n).mount('#app');
|
readyI18n().then(() => {
|
||||||
|
createApp(XrayPage).use(Antd).use(i18n).mount('#app');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,54 @@
|
||||||
// vue-i18n setup. Locale files live in web/translation/*.json — the same
|
|
||||||
// directory the Go binary embeds, so SPA + Telegram bot + subscription
|
|
||||||
// page all read from a single source.
|
|
||||||
//
|
|
||||||
// Usage in a component:
|
|
||||||
// import { useI18n } from 'vue-i18n';
|
|
||||||
// const { t } = useI18n();
|
|
||||||
// ...
|
|
||||||
// <span>{{ t('pages.inbounds.email') }}</span>
|
|
||||||
//
|
|
||||||
// Or via the global helper exposed on the app:
|
|
||||||
// <span>{{ $t('pages.inbounds.email') }}</span>
|
|
||||||
//
|
|
||||||
// The locale follows the `lang` cookie that LanguageManager already
|
|
||||||
// reads/writes — switching language anywhere in the app continues to
|
|
||||||
// trigger a full page reload (matches legacy ergonomics), so we don't
|
|
||||||
// need a runtime locale switcher here.
|
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { LanguageManager } from '@/utils';
|
import { LanguageManager } from '@/utils';
|
||||||
|
import enUS from '../../../web/translation/en-US.json';
|
||||||
|
|
||||||
// Lazy-loaded locales — Vite splits each one into its own chunk. We
|
|
||||||
// eager-load only the active language plus the en-US fallback so the
|
|
||||||
// initial page payload stays small (the inbounds bundle was sitting
|
|
||||||
// at ~700kB gzipped with all 13 locales eager; now ~480kB).
|
|
||||||
//
|
|
||||||
// LanguageManager.setLanguage() does a full reload on change, so
|
|
||||||
// "lazy" here effectively means "load only what this page needs for
|
|
||||||
// its lifetime."
|
|
||||||
const FALLBACK = 'en-US';
|
const FALLBACK = 'en-US';
|
||||||
const lazyModules = import.meta.glob('../../../web/translation/*.json');
|
const lazyModules = import.meta.glob([
|
||||||
const eagerModules = import.meta.glob('../../../web/translation/*.json', { eager: true });
|
'../../../web/translation/*.json',
|
||||||
|
'!../../../web/translation/en-US.json',
|
||||||
|
]);
|
||||||
|
|
||||||
function moduleKeyFor(code) {
|
function moduleKeyFor(code) {
|
||||||
return `../../../web/translation/${code}.json`;
|
return `../../../web/translation/${code}.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the active locale via LanguageManager so the cookie set on
|
|
||||||
// the legacy panel keeps working after a user upgrades. Falls back
|
|
||||||
// to en-US when the cookie names a language we don't have.
|
|
||||||
let active = LanguageManager.getLanguage();
|
let active = LanguageManager.getLanguage();
|
||||||
if (!Object.prototype.hasOwnProperty.call(lazyModules, moduleKeyFor(active))) {
|
if (active !== FALLBACK && !Object.prototype.hasOwnProperty.call(lazyModules, moduleKeyFor(active))) {
|
||||||
active = FALLBACK;
|
active = FALLBACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = {};
|
|
||||||
// Eagerly include the active locale + the fallback (when distinct)
|
|
||||||
// so the very first render has strings ready. Vite still emits these
|
|
||||||
// as their own chunks so the user pays for at most two locales.
|
|
||||||
for (const code of new Set([active, FALLBACK])) {
|
|
||||||
const mod = eagerModules[moduleKeyFor(code)];
|
|
||||||
if (mod) messages[code] = mod.default || mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
// `composition` mode (legacy: false) so `useI18n()` works in
|
|
||||||
// <script setup> blocks.
|
|
||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
locale: active,
|
locale: active,
|
||||||
fallbackLocale: FALLBACK,
|
fallbackLocale: FALLBACK,
|
||||||
// Locale JSON is nested by namespace ({pages: {inbounds: {email: ...}}})
|
messages: { [FALLBACK]: enUS },
|
||||||
// so vue-i18n's default `.`-delimited lookups walk straight into it.
|
|
||||||
messages,
|
|
||||||
// The Go side sometimes interpolates `#variable#` into translated
|
|
||||||
// strings (e.g. xraySwitchVersionDialogDesc). vue-i18n's default
|
|
||||||
// expects `{var}` — disable warnings about strings that look like
|
|
||||||
// they don't use the new syntax.
|
|
||||||
warnHtmlMessage: false,
|
warnHtmlMessage: false,
|
||||||
missingWarn: false,
|
missingWarn: false,
|
||||||
fallbackWarn: false,
|
fallbackWarn: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convenience export for non-component contexts (HTTP error toasts,
|
|
||||||
// stores, etc.) that need to look up a translation outside a setup
|
|
||||||
// scope.
|
|
||||||
export function t(key, params) {
|
export function t(key, params) {
|
||||||
return i18n.global.t(key, params || {});
|
return i18n.global.t(key, params || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadLocale fetches a locale module on demand and registers it with
|
|
||||||
// vue-i18n. Pages that switch language at runtime (rather than via
|
|
||||||
// LanguageManager's reload) can call this to swap strings live.
|
|
||||||
export async function loadLocale(code) {
|
export async function loadLocale(code) {
|
||||||
const key = moduleKeyFor(code);
|
if (code === FALLBACK) {
|
||||||
const loader = lazyModules[key];
|
i18n.global.locale.value = FALLBACK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const loader = lazyModules[moduleKeyFor(code)];
|
||||||
if (!loader) return false;
|
if (!loader) return false;
|
||||||
const mod = await loader();
|
const mod = await loader();
|
||||||
i18n.global.setLocaleMessage(code, mod.default || mod);
|
i18n.global.setLocaleMessage(code, mod.default || mod);
|
||||||
i18n.global.locale.value = code;
|
i18n.global.locale.value = code;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readyI18n() {
|
||||||
|
if (active !== FALLBACK) {
|
||||||
|
await loadLocale(active);
|
||||||
|
}
|
||||||
|
return i18n;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue