mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-17 00:05:56 +03: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:
@@ -4,7 +4,7 @@ import 'ant-design-vue/dist/reset.css';
|
||||
|
||||
import { setupAxios } from '@/api/axios-init.js';
|
||||
import '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import ApiDocsPage from '@/pages/api-docs/ApiDocsPage.vue';
|
||||
|
||||
@@ -16,4 +16,6 @@ if (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 '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import InboundsPage from '@/pages/inbounds/InboundsPage.vue';
|
||||
|
||||
@@ -16,4 +16,6 @@ if (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
|
||||
// stored theme to <body>/<html> before Vue mounts.
|
||||
import '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import IndexPage from '@/pages/index/IndexPage.vue';
|
||||
|
||||
@@ -18,4 +18,6 @@ if (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
|
||||
// stored theme to <body>/<html> before Vue renders anything.
|
||||
import '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import LoginPage from '@/pages/login/LoginPage.vue';
|
||||
|
||||
setupAxios();
|
||||
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');
|
||||
if (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 '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import NodesPage from '@/pages/nodes/NodesPage.vue';
|
||||
|
||||
@@ -16,4 +16,6 @@ if (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
|
||||
// stored theme to <body>/<html> before Vue mounts.
|
||||
import '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import SettingsPage from '@/pages/settings/SettingsPage.vue';
|
||||
|
||||
@@ -18,4 +18,6 @@ if (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
|
||||
// share links — the SPA reads those at mount.
|
||||
import '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import SubPage from '@/pages/sub/SubPage.vue';
|
||||
|
||||
const messageContainer = document.getElementById('message');
|
||||
@@ -15,4 +15,6 @@ if (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 '@/composables/useTheme.js';
|
||||
import { i18n } from '@/i18n/index.js';
|
||||
import { i18n, readyI18n } from '@/i18n/index.js';
|
||||
import { applyDocumentTitle } from '@/utils';
|
||||
import XrayPage from '@/pages/xray/XrayPage.vue';
|
||||
|
||||
@@ -16,4 +16,6 @@ if (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 { 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 lazyModules = import.meta.glob('../../../web/translation/*.json');
|
||||
const eagerModules = import.meta.glob('../../../web/translation/*.json', { eager: true });
|
||||
const lazyModules = import.meta.glob([
|
||||
'../../../web/translation/*.json',
|
||||
'!../../../web/translation/en-US.json',
|
||||
]);
|
||||
|
||||
function moduleKeyFor(code) {
|
||||
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();
|
||||
if (!Object.prototype.hasOwnProperty.call(lazyModules, moduleKeyFor(active))) {
|
||||
if (active !== FALLBACK && !Object.prototype.hasOwnProperty.call(lazyModules, moduleKeyFor(active))) {
|
||||
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({
|
||||
legacy: false,
|
||||
// `composition` mode (legacy: false) so `useI18n()` works in
|
||||
// <script setup> blocks.
|
||||
globalInjection: true,
|
||||
locale: active,
|
||||
fallbackLocale: FALLBACK,
|
||||
// Locale JSON is nested by namespace ({pages: {inbounds: {email: ...}}})
|
||||
// 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.
|
||||
messages: { [FALLBACK]: enUS },
|
||||
warnHtmlMessage: false,
|
||||
missingWarn: 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) {
|
||||
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) {
|
||||
const key = moduleKeyFor(code);
|
||||
const loader = lazyModules[key];
|
||||
if (code === FALLBACK) {
|
||||
i18n.global.locale.value = FALLBACK;
|
||||
return true;
|
||||
}
|
||||
const loader = lazyModules[moduleKeyFor(code)];
|
||||
if (!loader) return false;
|
||||
const mod = await loader();
|
||||
i18n.global.setLocaleMessage(code, mod.default || mod);
|
||||
i18n.global.locale.value = code;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function readyI18n() {
|
||||
if (active !== FALLBACK) {
|
||||
await loadLocale(active);
|
||||
}
|
||||
return i18n;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user