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