mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 17:46:02 +00:00
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>
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
// Converts the panel's TOML translation files (web/translation/) into
|
|
// nested JSON that vue-i18n can consume. The Go side stays the source
|
|
// of truth — translators continue to edit the TOML files; this script
|
|
// snapshots them into frontend/src/locales/<code>.json on each run.
|
|
//
|
|
// Run via `npm run i18n:sync` (also kicked off automatically by
|
|
// `npm run prebuild` so production builds always include the latest
|
|
// strings).
|
|
//
|
|
// Format support is intentionally narrow — the project's TOML files
|
|
// are limited to:
|
|
// • blank lines and `# comment` lines
|
|
// • bare-string values: "key" = "value"
|
|
// • dotted section heads: [pages.inbounds.toasts]
|
|
// Multi-line strings, arrays, dates, and inline tables aren't used in
|
|
// the panel's translation set, so the parser rejects them rather than
|
|
// silently mis-parsing. If the format ever grows, swap this out for a
|
|
// proper TOML lib.
|
|
|
|
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
import { resolve, dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const tomlDir = resolve(here, '..', '..', 'web', 'translation');
|
|
const outDir = resolve(here, '..', 'src', 'locales');
|
|
|
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
|
|
// Decode the small set of escapes TOML allows inside basic strings.
|
|
// Unicode `\uXXXX` escapes aren't used in the panel's files but are
|
|
// handled too just in case a translator adds one.
|
|
function unescape(value) {
|
|
return value.replace(/\\(["\\bfnrt]|u[0-9a-fA-F]{4})/g, (_m, what) => {
|
|
if (what === '"') return '"';
|
|
if (what === '\\') return '\\';
|
|
if (what === 'b') return '\b';
|
|
if (what === 'f') return '\f';
|
|
if (what === 'n') return '\n';
|
|
if (what === 'r') return '\r';
|
|
if (what === 't') return '\t';
|
|
return String.fromCharCode(parseInt(what.slice(1), 16));
|
|
});
|
|
}
|
|
|
|
function setNested(target, path, value) {
|
|
let cursor = target;
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
const seg = path[i];
|
|
if (typeof cursor[seg] !== 'object' || cursor[seg] === null) {
|
|
cursor[seg] = {};
|
|
}
|
|
cursor = cursor[seg];
|
|
}
|
|
cursor[path[path.length - 1]] = value;
|
|
}
|
|
|
|
const SECTION_RE = /^\[([A-Za-z0-9_.-]+)\]$/;
|
|
const KV_RE = /^"([^"\\]*(?:\\.[^"\\]*)*)"\s*=\s*"((?:[^"\\]|\\.)*)"$/;
|
|
|
|
function parseToml(src) {
|
|
const tree = {};
|
|
let section = [];
|
|
let lineNo = 0;
|
|
for (const rawLine of src.split(/\r?\n/)) {
|
|
lineNo++;
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) continue;
|
|
|
|
const sectionMatch = SECTION_RE.exec(line);
|
|
if (sectionMatch) {
|
|
section = sectionMatch[1].split('.');
|
|
continue;
|
|
}
|
|
|
|
const kvMatch = KV_RE.exec(line);
|
|
if (!kvMatch) {
|
|
throw new Error(`Unsupported TOML construct at line ${lineNo}: ${rawLine}`);
|
|
}
|
|
const [, key, value] = kvMatch;
|
|
setNested(tree, [...section, unescape(key)], unescape(value));
|
|
}
|
|
return tree;
|
|
}
|
|
|
|
const files = readdirSync(tomlDir).filter((f) => f.startsWith('translate.') && f.endsWith('.toml'));
|
|
let count = 0;
|
|
for (const file of files) {
|
|
const code = file.replace(/^translate\./, '').replace(/\.toml$/, '').replace('_', '-');
|
|
const tree = parseToml(readFileSync(join(tomlDir, file), 'utf8'));
|
|
const outPath = join(outDir, `${code}.json`);
|
|
writeFileSync(outPath, JSON.stringify(tree, null, 2) + '\n');
|
|
count++;
|
|
}
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`sync-locales: wrote ${count} locale file(s) to ${outDir}`);
|