Files
3x-ui/frontend/src/i18n/index.js
T
MHSanaei 35efeb983e feat(frontend): Phase 7 — vue-i18n wired up + login page translated
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>
2026-05-08 14:54:07 +02:00

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;
}