Improve first-run and channel setup for non-technical users: detect newer Gemini key formats, pin Node 22.22.3, add Control Panel Telegram approve flow, and keep channels/models when config is rewritten. Persist uclaw wizard state via uclaw-meta.json so restarts skip language/persona prompts. Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
2.9 KiB
JavaScript
94 lines
2.9 KiB
JavaScript
// Message catalogue for the launcher and the local HTML pages.
|
|
//
|
|
// Locale resolution follows one rule: the language travels with the drive, not
|
|
// with the machine. Someone who set up the drive in Chinese and plugs it into a
|
|
// colleague's English Windows should still see Chinese.
|
|
//
|
|
// explicit override > drive config > system locale > en
|
|
//
|
|
// Keys are semantic (`start.node_missing`), never the source string, so changing
|
|
// the English wording does not invalidate every other catalogue.
|
|
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const SUPPORTED = ['en', 'zh-CN'];
|
|
const FALLBACK = 'en';
|
|
|
|
function normalise(tag) {
|
|
if (!tag) return null;
|
|
const lower = String(tag).toLowerCase();
|
|
if (lower.startsWith('zh')) return 'zh-CN';
|
|
if (lower.startsWith('en')) return 'en';
|
|
return null;
|
|
}
|
|
|
|
// Reads uclaw.locale out of the drive's own config. A malformed or missing file
|
|
// is not an error — it just means we have no preference yet.
|
|
function localeFromConfig(configPath) {
|
|
if (!configPath || !existsSync(configPath)) return null;
|
|
try {
|
|
return normalise(JSON.parse(readFileSync(configPath, 'utf8'))?.uclaw?.locale);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function localeFromSystem() {
|
|
const env = process.env.UCLAW_LOCALE || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG;
|
|
if (env) return normalise(env.split('.')[0].replace('_', '-'));
|
|
try {
|
|
return normalise(new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function localeFromMeta(configPath) {
|
|
if (!configPath) return null;
|
|
try {
|
|
const metaPath = join(dirname(configPath), 'uclaw-meta.json');
|
|
if (!existsSync(metaPath)) return null;
|
|
return normalise(JSON.parse(readFileSync(metaPath, 'utf8'))?.locale);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function resolveLocale({ override, configPath } = {}) {
|
|
return (
|
|
normalise(override) ||
|
|
localeFromConfig(configPath) ||
|
|
localeFromMeta(configPath) ||
|
|
localeFromSystem() ||
|
|
FALLBACK
|
|
);
|
|
}
|
|
|
|
function loadCatalogue(locale) {
|
|
try {
|
|
return JSON.parse(readFileSync(join(here, 'messages', `${locale}.json`), 'utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function createTranslator(locale) {
|
|
const active = SUPPORTED.includes(locale) ? locale : FALLBACK;
|
|
const catalogue = loadCatalogue(active) ?? {};
|
|
// Always keep English loaded: a key missing from a translation should fall back
|
|
// to readable English, never to a raw key leaking into the UI.
|
|
const fallback = active === FALLBACK ? catalogue : (loadCatalogue(FALLBACK) ?? {});
|
|
|
|
return function t(key, vars = {}) {
|
|
const template = catalogue[key] ?? fallback[key] ?? key;
|
|
return template.replace(/\{(\w+)\}/g, (match, name) =>
|
|
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
|
|
);
|
|
};
|
|
}
|
|
|
|
export { SUPPORTED, FALLBACK };
|