Files
u-claw/portable/lib/i18n.mjs
zheng dd8e0f55c3 fix(portable): Gemini keys, Telegram pairing UI, and config durability
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>
2026-08-18 18:45:03 +08:00

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