From 79a9be7b22ab5bbf40d81cdc9bcc3edc1e417e4a Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 15 May 2026 10:50:40 +0200 Subject: [PATCH] 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. --- frontend/src/entries/api-docs.js | 6 ++- frontend/src/entries/inbounds.js | 6 ++- frontend/src/entries/index.js | 6 ++- frontend/src/entries/login.js | 8 ++-- frontend/src/entries/nodes.js | 6 ++- frontend/src/entries/settings.js | 6 ++- frontend/src/entries/subpage.js | 6 ++- frontend/src/entries/xray.js | 6 ++- frontend/src/i18n/index.js | 77 ++++++++------------------------ 9 files changed, 51 insertions(+), 76 deletions(-) diff --git a/frontend/src/entries/api-docs.js b/frontend/src/entries/api-docs.js index 852bbc41..c2baff6c 100644 --- a/frontend/src/entries/api-docs.js +++ b/frontend/src/entries/api-docs.js @@ -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'); +}); diff --git a/frontend/src/entries/inbounds.js b/frontend/src/entries/inbounds.js index 8342f476..dd9b8170 100644 --- a/frontend/src/entries/inbounds.js +++ b/frontend/src/entries/inbounds.js @@ -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'); +}); diff --git a/frontend/src/entries/index.js b/frontend/src/entries/index.js index 8e14d2ae..33593f31 100644 --- a/frontend/src/entries/index.js +++ b/frontend/src/entries/index.js @@ -6,7 +6,7 @@ import { setupAxios } from '@/api/axios-init.js'; // Importing useTheme triggers the boot side-effect that applies the // stored theme to / 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'); +}); diff --git a/frontend/src/entries/login.js b/frontend/src/entries/login.js index 6b8b0fc2..33c1e629 100644 --- a/frontend/src/entries/login.js +++ b/frontend/src/entries/login.js @@ -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 / 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'); +}); diff --git a/frontend/src/entries/nodes.js b/frontend/src/entries/nodes.js index 819fe9a9..92e60a15 100644 --- a/frontend/src/entries/nodes.js +++ b/frontend/src/entries/nodes.js @@ -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'); +}); diff --git a/frontend/src/entries/settings.js b/frontend/src/entries/settings.js index 4a32bb7e..ca3e6f29 100644 --- a/frontend/src/entries/settings.js +++ b/frontend/src/entries/settings.js @@ -6,7 +6,7 @@ import { setupAxios } from '@/api/axios-init.js'; // Importing useTheme triggers the boot side-effect that applies the // stored theme to / 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'); +}); diff --git a/frontend/src/entries/subpage.js b/frontend/src/entries/subpage.js index 159aee1b..a2b0d8bb 100644 --- a/frontend/src/entries/subpage.js +++ b/frontend/src/entries/subpage.js @@ -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'); +}); diff --git a/frontend/src/entries/xray.js b/frontend/src/entries/xray.js index ba203da0..a5b1ebcd 100644 --- a/frontend/src/entries/xray.js +++ b/frontend/src/entries/xray.js @@ -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'); +}); diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js index 1e21d215..7b13784c 100644 --- a/frontend/src/i18n/index.js +++ b/frontend/src/i18n/index.js @@ -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(); -// ... -// {{ t('pages.inbounds.email') }} -// -// Or via the global helper exposed on the app: -// {{ $t('pages.inbounds.email') }} -// -// 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 - //