mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-14 16:46:07 +00:00
35efeb983e
Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
95 lines
3.5 KiB
JavaScript
95 lines
3.5 KiB
JavaScript
// vue-i18n setup. Locale files are generated from web/translation/*.toml
|
|
// by `npm run i18n:sync` (run automatically as a pre-build step).
|
|
//
|
|
// 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';
|
|
|
|
// 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('../locales/*.json');
|
|
const eagerModules = import.meta.glob('../locales/*.json', { eager: true });
|
|
|
|
function moduleKeyFor(code) {
|
|
return `../locales/${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))) {
|
|
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,
|
|
// Existing TOML keys contain `.` for nested-path lookups, which is
|
|
// exactly what vue-i18n's default `pathDelimiter` does. Keep the
|
|
// delimiter and let lookups like `t('pages.inbounds.email')` walk
|
|
// the nested object the sync script wrote.
|
|
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,
|
|
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 = `../locales/${code}.json`;
|
|
const loader = lazyModules[key];
|
|
if (!loader) return false;
|
|
const mod = await loader();
|
|
i18n.global.setLocaleMessage(code, mod.default || mod);
|
|
i18n.global.locale.value = code;
|
|
return true;
|
|
}
|