From e3b9fb2c2e7671ff71c3086e5fcae7679cc33658 Mon Sep 17 00:00:00 2001 From: jerryfan Date: Fri, 1 May 2026 15:22:45 +0800 Subject: [PATCH] Localize security mode decision copy --- extensions/i18n.ts | 90 ++++++++++++++++++++++++++++++++++++++++++ extensions/security.ts | 36 +++++++++-------- 2 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 extensions/i18n.ts diff --git a/extensions/i18n.ts b/extensions/i18n.ts new file mode 100644 index 0000000..2213e12 --- /dev/null +++ b/extensions/i18n.ts @@ -0,0 +1,90 @@ +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +type Locale = "en" | "es" | "fr" | "pt-BR"; +type Params = Record; + +const translations: Record, Record> = { + es: { + "security.mode.current": "Modo actual: {mode}", + "security.config.path": "Ruta de configuración: {path}", + "security.mode.basic": "Básico: comandos críticos bloqueados, localhost/127.x permitido", + "security.mode.max": "Máximo: todos los comandos bloqueados, protección SSRF completa", + "security.mode.setBasic": "/security mode basic — relajar restricciones para desarrollo", + "security.mode.setMax": "/security mode max — bloqueo completo (predeterminado)", + "security.alreadyMode": "El modo de seguridad ya es {mode}", + "security.persistFailed": "ERROR al guardar el modo de seguridad: no se pudo escribir {path}", + "security.modeChanged": "Modo de seguridad cambiado a {mode}", + "security.invalidMode": "Modo no válido: \"{mode}\". Usa \"basic\" o \"max\".", + "security.extendedAllowed": "Los comandos extendidos ahora están PERMITIDOS: rm, sudo, npm, apt, git, curl, wget, etc.", + "security.localhostAllowed": "Las URLs localhost y 127.x ahora están PERMITIDAS para SSRF", + "security.criticalStillBlocked": "Los comandos críticos siguen bloqueados: dd, mkfs, shred, fdisk, ssh, etc.", + "security.fullLockdown": "Bloqueo completo activo — los {count} comandos están bloqueados", + "security.fullSsrf": "Protección SSRF completa — localhost e IPs privadas bloqueadas", + "security.auditRequiresTui": "La auditoría de seguridad requiere modo TUI", + }, + fr: { + "security.mode.current": "Mode actuel : {mode}", + "security.config.path": "Chemin de configuration : {path}", + "security.mode.basic": "Basique : commandes critiques bloquées, localhost/127.x autorisé", + "security.mode.max": "Maximum : toutes les commandes bloquées, protection SSRF complète", + "security.mode.setBasic": "/security mode basic — assouplir les restrictions pour le développement", + "security.mode.setMax": "/security mode max — verrouillage complet (par défaut)", + "security.alreadyMode": "Le mode de sécurité est déjà {mode}", + "security.persistFailed": "ÉCHEC de l’enregistrement du mode de sécurité : impossible d’écrire {path}", + "security.modeChanged": "Mode de sécurité défini sur {mode}", + "security.invalidMode": "Mode non valide : \"{mode}\". Utilisez \"basic\" ou \"max\".", + "security.extendedAllowed": "Les commandes étendues sont maintenant AUTORISÉES : rm, sudo, npm, apt, git, curl, wget, etc.", + "security.localhostAllowed": "Les URL localhost et 127.x sont maintenant AUTORISÉES pour SSRF", + "security.criticalStillBlocked": "Les commandes critiques restent bloquées : dd, mkfs, shred, fdisk, ssh, etc.", + "security.fullLockdown": "Verrouillage complet actif — les {count} commandes sont bloquées", + "security.fullSsrf": "Protection SSRF complète — localhost et IP privées bloqués", + "security.auditRequiresTui": "L’audit de sécurité nécessite le mode TUI", + }, + "pt-BR": { + "security.mode.current": "Modo atual: {mode}", + "security.config.path": "Caminho de configuração: {path}", + "security.mode.basic": "Básico: comandos críticos bloqueados, localhost/127.x permitido", + "security.mode.max": "Máximo: todos os comandos bloqueados, proteção SSRF completa", + "security.mode.setBasic": "/security mode basic — relaxar restrições para desenvolvimento", + "security.mode.setMax": "/security mode max — bloqueio completo (padrão)", + "security.alreadyMode": "O modo de segurança já é {mode}", + "security.persistFailed": "FALHA ao persistir o modo de segurança: não foi possível escrever {path}", + "security.modeChanged": "Modo de segurança definido para {mode}", + "security.invalidMode": "Modo inválido: \"{mode}\". Use \"basic\" ou \"max\".", + "security.extendedAllowed": "Comandos estendidos agora estão PERMITIDOS: rm, sudo, npm, apt, git, curl, wget, etc.", + "security.localhostAllowed": "URLs localhost e 127.x agora estão PERMITIDAS para SSRF", + "security.criticalStillBlocked": "Comandos críticos continuam bloqueados: dd, mkfs, shred, fdisk, ssh, etc.", + "security.fullLockdown": "Bloqueio completo ativo — todos os {count} comandos bloqueados", + "security.fullSsrf": "Proteção SSRF completa — localhost e IPs privados bloqueados", + "security.auditRequiresTui": "A auditoria de segurança requer modo TUI", + }, +}; + +let currentLocale: Locale = "en"; + +export function initI18n(pi: ExtensionAPI): void { + pi.events?.emit?.("pi-core/i18n/registerBundle", { + namespace: "vtstech-security", + defaultLocale: "en", + locales: translations, + }); + + pi.events?.emit?.("pi-core/i18n/requestApi", { + onReady: (api: { getLocale?: () => string; onLocaleChange?: (cb: (locale: string) => void) => void }) => { + const next = api.getLocale?.(); + if (isLocale(next)) currentLocale = next; + api.onLocaleChange?.((locale) => { + if (isLocale(locale)) currentLocale = locale; + }); + }, + }); +} + +export function t(key: string, fallback: string, params: Params = {}): string { + const template = currentLocale === "en" ? fallback : translations[currentLocale]?.[key] ?? fallback; + return template.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`)); +} + +function isLocale(locale: string | undefined): locale is Locale { + return locale === "en" || locale === "es" || locale === "fr" || locale === "pt-BR"; +} diff --git a/extensions/security.ts b/extensions/security.ts index 66e2eb6..25739a5 100644 --- a/extensions/security.ts +++ b/extensions/security.ts @@ -40,6 +40,7 @@ import { import { debugLog } from "../shared/debug"; import { section, ok, fail, warn, info } from "../shared/format"; import { EXTENSION_VERSION } from "../shared/ollama"; +import { initI18n, t } from "./i18n"; // ── Types ──────────────────────────────────────────────────────────────── @@ -54,6 +55,7 @@ interface SecurityStats { // ── Extension ──────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { + initI18n(pi); const stats: SecurityStats = { blocked: 0, allowed: 0, @@ -92,19 +94,19 @@ export default function (pi: ExtensionAPI) { if (!value) { const lines: string[] = [branding]; lines.push(section("SECURITY MODE")); - lines.push(info(`Current mode: ${currentMode.toUpperCase()}`)); - lines.push(info(`Config path: ${SECURITY_CONFIG_PATH}`)); + lines.push(info(t("security.mode.current", "Current mode: {mode}", { mode: currentMode.toUpperCase() }))); + lines.push(info(t("security.config.path", "Config path: {path}", { path: SECURITY_CONFIG_PATH }))); lines.push(info(`Critical commands (always blocked): ${CRITICAL_COMMANDS.size}`)); lines.push(info(`Extended commands (max only): ${EXTENDED_COMMANDS.size}`)); lines.push(info(`Total blocked (max): ${CRITICAL_COMMANDS.size + EXTENDED_COMMANDS.size}`)); lines.push(info(`URL patterns always blocked: ${BLOCKED_URL_ALWAYS.size}`)); lines.push(info(`URL patterns (max only): ${BLOCKED_URL_MAX_ONLY.size}`)); lines.push(section("MODE DIFFERENCES")); - lines.push(info("Basic: critical commands blocked, localhost/127.x allowed")); - lines.push(info("Max: all commands blocked, full SSRF protection")); + lines.push(info(t("security.mode.basic", "Basic: critical commands blocked, localhost/127.x allowed"))); + lines.push(info(t("security.mode.max", "Max: all commands blocked, full SSRF protection"))); lines.push(section("SWITCH MODE")); - lines.push(info("/security mode basic — relax restrictions for development")); - lines.push(info("/security mode max — full lockdown (default)")); + lines.push(info(t("security.mode.setBasic", "/security mode basic — relax restrictions for development"))); + lines.push(info(t("security.mode.setMax", "/security mode max — full lockdown (default)"))); lines.push(branding); pi.sendMessage({ @@ -118,19 +120,19 @@ export default function (pi: ExtensionAPI) { // /security mode basic|max if (value === "basic" || value === "max") { if (value === currentMode) { - ctx.ui.notify(`Security mode is already ${value.toUpperCase()}`, "info"); + ctx.ui.notify(t("security.alreadyMode", "Security mode is already {mode}", { mode: value.toUpperCase() }), "info"); return; } const writeOk = setSecurityMode(value as "basic" | "max"); if (!writeOk) { - ctx.ui.notify(`FAILED to persist security mode: could not write ${SECURITY_CONFIG_PATH}`, "error"); + ctx.ui.notify(t("security.persistFailed", "FAILED to persist security mode: could not write {path}", { path: SECURITY_CONFIG_PATH }), "error"); debugLog("security", `/security mode ${value}: write failed`, { path: SECURITY_CONFIG_PATH }); return; } ctx.ui.setStatus("status-sec", value.toUpperCase()); - ctx.ui.notify(`Security mode set to ${value.toUpperCase()}`, "success"); + ctx.ui.notify(t("security.modeChanged", "Security mode set to {mode}", { mode: value.toUpperCase() }), "success"); appendAuditEntry({ timestamp: new Date().toISOString(), @@ -149,12 +151,12 @@ export default function (pi: ExtensionAPI) { lines.push(info(`Config: ${SECURITY_CONFIG_PATH}`)); if (value === "basic") { - lines.push(warn("Extended commands are now ALLOWED: rm, sudo, npm, apt, git, curl, wget, etc.")); - lines.push(warn("Localhost and 127.x URLs are now ALLOWED for SSRF")); - lines.push(ok("Critical commands remain blocked: dd, mkfs, shred, fdisk, ssh, etc.")); + lines.push(warn(t("security.extendedAllowed", "Extended commands are now ALLOWED: rm, sudo, npm, apt, git, curl, wget, etc."))); + lines.push(warn(t("security.localhostAllowed", "Localhost and 127.x URLs are now ALLOWED for SSRF"))); + lines.push(ok(t("security.criticalStillBlocked", "Critical commands remain blocked: dd, mkfs, shred, fdisk, ssh, etc."))); } else { - lines.push(ok(`Full lockdown active — all ${totalCmds} commands blocked`)); - lines.push(ok("Full SSRF protection — localhost and private IPs blocked")); + lines.push(ok(t("security.fullLockdown", "Full lockdown active — all {count} commands blocked", { count: totalCmds }))); + lines.push(ok(t("security.fullSsrf", "Full SSRF protection — localhost and private IPs blocked"))); } lines.push(branding); @@ -168,7 +170,7 @@ export default function (pi: ExtensionAPI) { } // Invalid mode value - ctx.ui.notify(`Invalid mode: "${value}". Use \"basic\" or \"max\".`, "error"); + ctx.ui.notify(t("security.invalidMode", "Invalid mode: \"{mode}\". Use \"basic\" or \"max\".", { mode: value || "" }), "error"); return; } @@ -342,7 +344,7 @@ export default function (pi: ExtensionAPI) { // Security mode lines.push(section("SECURITY MODE")); - lines.push(info(`Current mode: ${currentMode.toUpperCase()}`)); + lines.push(info(t("security.mode.current", "Current mode: {mode}", { mode: currentMode.toUpperCase() }))); lines.push(info(`Config file: ${SECURITY_CONFIG_PATH}`)); // Effective blocklist summary @@ -428,7 +430,7 @@ export default function (pi: ExtensionAPI) { description: "Show security audit report — blocked operations, stats, and recent log", handler: async (_args, ctx) => { if (!ctx.hasUI) { - ctx.ui.notify("Security audit requires TUI mode", "error"); + ctx.ui.notify(t("security.auditRequiresTui", "Security audit requires TUI mode"), "error"); return; } try {