Files
OmniRoute/bin/cli/i18n.mjs
diegosouzapw 2e494f8f07 feat(cli): Fase 0.3 — helpers base + convenções (api, i18n, output, runtime)
- bin/cli/CONVENTIONS.md: fonte normativa de flags, exit codes, output,
  retry/backoff, i18n, secrets, auditoria de ações destrutivas
- bin/cli/api.mjs: apiFetch() com retry/backoff, Retry-After, ApiError,
  statusToExitCode, isServerUp; computeBackoff/shouldRetryStatus exportados
- bin/cli/runtime.mjs: withRuntime/withHttp/withDb — server-first / DB-fallback;
  ServerOfflineError com exitCode 3
- bin/cli/i18n.mjs: t() com Map achatado (sem bracket em prototype), interpolação
  {vars}, setLocale/detectLocale/resetForTests; hardened contra __proto__ traversal
- bin/cli/output.mjs: emit() (table/json/jsonl/csv), EXIT_CODES, maskSecret,
  printSuccess/printError/printWarning/exitWith; output → stdout, diagnóstico → stderr
- bin/cli/locales/en.json + pt-BR.json: strings base (setup/doctor/providers/
  keys/combo/serve/backup/update/health/mcp/tunnel)
- bin/cli/README.md: mapa da estrutura e guia de uso dos helpers
- tests/unit/cli-exit-codes.test.ts: 10 casos — EXIT_CODES, statusToExitCode,
  backoff exponencial, jitter ±25%, t() i18n com pt-BR e anti-__proto__
- .env.example + docs/reference/ENVIRONMENT.md: documentar 4 novas env vars CLI
  (OMNIROUTE_LANG, OMNIROUTE_CLI_TOKEN, OMNIROUTE_HTTP_TIMEOUT_MS, OMNIROUTE_VERBOSE)
- scripts/check/check-env-doc-sync.mjs: adicionar LC_MESSAGES ao allowlist de sistema
2026-05-14 21:42:57 -03:00

107 lines
2.9 KiB
JavaScript

import { readFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = join(__dirname, "locales");
const FALLBACK_LOCALE = "en";
const cache = new Map();
let activeLocale = null;
let fallbackCatalog = null;
export function detectLocale() {
const raw =
process.env.OMNIROUTE_LANG ||
process.env.LC_ALL ||
process.env.LC_MESSAGES ||
process.env.LANG ||
FALLBACK_LOCALE;
return normalize(raw);
}
function normalize(raw) {
const stripped = String(raw).split(".")[0].replace("_", "-");
if (!stripped) return FALLBACK_LOCALE;
if (hasCatalog(stripped)) return stripped;
const base = stripped.split("-")[0];
if (hasCatalog(base)) return base;
return FALLBACK_LOCALE;
}
function hasCatalog(locale) {
return existsSync(join(LOCALES_DIR, `${locale}.json`));
}
function flattenToMap(obj, prefix, result) {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
flattenToMap(value, fullKey, result);
} else if (typeof value === "string") {
result.set(fullKey, value);
}
}
}
function loadCatalog(locale) {
if (cache.has(locale)) return cache.get(locale);
const file = join(LOCALES_DIR, `${locale}.json`);
if (!existsSync(file)) {
cache.set(locale, null);
return null;
}
try {
const parsed = JSON.parse(readFileSync(file, "utf8"));
const flat = new Map();
flattenToMap(parsed, "", flat);
cache.set(locale, flat);
return flat;
} catch {
cache.set(locale, null);
return null;
}
}
export function setLocale(locale) {
activeLocale = normalize(locale);
loadCatalog(activeLocale);
return activeLocale;
}
export function getLocale() {
if (!activeLocale) activeLocale = detectLocale();
return activeLocale;
}
function interpolate(template, vars) {
if (!vars) return template;
const entries = Object.entries(vars);
if (entries.length === 0) return template;
const varMap = new Map(entries);
return template.replace(/\{(\w+)\}/g, (match, name) => {
const v = varMap.get(name);
return v !== undefined ? String(v) : match;
});
}
export function t(key, vars) {
if (!activeLocale) activeLocale = detectLocale();
const primary = loadCatalog(activeLocale);
const fromPrimary = primary?.get(key);
if (fromPrimary !== undefined) return interpolate(fromPrimary, vars);
if (activeLocale !== FALLBACK_LOCALE) {
if (!fallbackCatalog) fallbackCatalog = loadCatalog(FALLBACK_LOCALE);
const fromFallback = fallbackCatalog?.get(key);
if (fromFallback !== undefined) return interpolate(fromFallback, vars);
}
return key;
}
export function resetForTests() {
cache.clear();
activeLocale = null;
fallbackCatalog = null;
}