import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; type Params = Record; const namespace = "pi-crew"; const TEMPLATE_RE = /\{(\w+)\}/g; const fallback = { "agent.requiresPrompt": "Agent requires prompt.", "agent.started": "Agent {state}.", "agent.id": "Agent ID: {id}", "agent.type": "Type: {type}", "agent.description": "Description: {description}", "agent.retrieveHint": "Use get_subagent_result to retrieve output. Do not duplicate this agent's work.", "agent.foregroundStatus": "Agent {id} {status}.", "agent.noOutput": "No output.", "result.requiresAgentId": "get_subagent_result requires agent_id.", "result.notFound": "Agent not found: {id}", "result.unrecoverable": "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", "result.waitAborted": "Waiting for subagent result was aborted.", "result.waitTimeout": "Timed out waiting for subagent result.", "result.stillRunning": "Agent is still running. Use wait=true or check again later.", "steer.noted": "Steering request noted for {id}.", "steer.unavailable": "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", "steer.cancelHint": "Use team cancel runId={runId} if the agent must be interrupted.", } as const; type Key = keyof typeof fallback; /** Map of locale → partial translations. Keys not present fall back to English. */ const translations: Record>> = { es: { "agent.requiresPrompt": "Agent requiere prompt.", "agent.started": "Agent {state}.", "agent.id": "ID del agente: {id}", "agent.type": "Tipo: {type}", "agent.description": "Descripción: {description}", "agent.retrieveHint": "Usa get_subagent_result para recuperar la salida. No dupliques el trabajo de este agente.", "agent.foregroundStatus": "Agent {id} {status}.", "agent.noOutput": "Sin salida.", "result.requiresAgentId": "get_subagent_result requiere agent_id.", "result.notFound": "Agente no encontrado: {id}", "result.unrecoverable": "El subagente fue interrumpido antes de registrar su ID de ejecución duradero; no se puede recuperar tras reiniciar.", "result.waitAborted": "Se canceló la espera del resultado del subagente.", "result.waitTimeout": "Se agotó el tiempo de espera del resultado del subagente.", "result.stillRunning": "El agente sigue ejecutándose. Usa wait=true o vuelve a comprobar más tarde.", "steer.noted": "Solicitud de dirección registrada para {id}.", "steer.unavailable": "El backend predeterminado actual de pi-crew es child-process, así que session.steer a mitad de turno aún no está disponible.", "steer.cancelHint": "Usa team cancel runId={runId} si hay que interrumpir el agente.", }, fr: { "agent.requiresPrompt": "Agent nécessite un prompt.", "agent.started": "Agent {state}.", "agent.id": "ID de l'agent : {id}", "agent.type": "Type : {type}", "agent.description": "Description : {description}", "agent.retrieveHint": "Utilisez get_subagent_result pour récupérer la sortie. Ne dupliquez pas le travail de cet agent.", "agent.foregroundStatus": "Agent {id} {status}.", "agent.noOutput": "Aucune sortie.", "result.requiresAgentId": "get_subagent_result nécessite agent_id.", "result.notFound": "Agent introuvable : {id}", "result.unrecoverable": "Le sous-agent a été interrompu avant l'enregistrement de son ID d'exécution durable ; il ne peut pas être récupéré après redémarrage.", "result.waitAborted": "L'attente du résultat du sous-agent a été annulée.", "result.waitTimeout": "Délai d'attente du résultat du sous-agent dépassé.", "result.stillRunning": "L'agent est toujours en cours d'exécution. Utilisez wait=true ou réessayez plus tard.", "steer.noted": "Demande de pilotage enregistrée pour {id}.", "steer.unavailable": "Le backend pi-crew par défaut actuel est child-process, donc session.steer en milieu de tour n'est pas encore disponible.", "steer.cancelHint": "Utilisez team cancel runId={runId} si l'agent doit être interrompu.", }, "pt-BR": { "agent.requiresPrompt": "Agent requer prompt.", "agent.started": "Agent {state}.", "agent.id": "ID do agente: {id}", "agent.type": "Tipo: {type}", "agent.description": "Descrição: {description}", "agent.retrieveHint": "Use get_subagent_result para recuperar a saída. Não duplique o trabalho deste agente.", "agent.foregroundStatus": "Agent {id} {status}.", "agent.noOutput": "Sem saída.", "result.requiresAgentId": "get_subagent_result requer agent_id.", "result.notFound": "Agente não encontrado: {id}", "result.unrecoverable": "O subagente foi interrompido antes que seu ID de execução durável fosse registrado; ele não pode ser recuperado após reiniciar.", "result.waitAborted": "A espera pelo resultado do subagente foi abortada.", "result.waitTimeout": "Tempo limite de espera pelo resultado do subagente esgotado.", "result.stillRunning": "O agente ainda está em execução. Use wait=true ou verifique novamente mais tarde.", "steer.noted": "Solicitação de orientação registrada para {id}.", "steer.unavailable": "O backend padrão atual do pi-crew é child-process, então session.steer no meio do turno ainda não está disponível.", "steer.cancelHint": "Use team cancel runId={runId} se o agente precisar ser interrompido.", }, }; // --- Runtime state --- let currentLocale: string | undefined; const warnedMissing = new Set(); // --- Helpers --- function format(template: string, params: Params = {}): string { return template.replace(TEMPLATE_RE, (_match, key) => String(params[key] ?? `{${key}}`)); } function warnOnce(key: string): void { const tag = `${currentLocale}:${key}`; if (warnedMissing.has(tag)) return; warnedMissing.add(tag); process.stderr.write(`[pi-crew] i18n: missing "${key}" in locale "${currentLocale}" — using English\n`); } // --- Public API --- /** * Translate a key for the currently active locale. * Falls back to English, then to the raw key as a last resort. */ export function t(key: Key, params?: Params): string { if (currentLocale && translations[currentLocale]) { const template = translations[currentLocale]?.[key]; if (template) return format(template, params); warnOnce(key); } return format(fallback[key] ?? key, params); } /** * Register or extend translations for a locale at runtime. * Useful for contributors adding new language bundles without modifying i18n.ts. * * @example * addTranslations("vi", { "agent.requiresPrompt": "Agent cần prompt." }) */ export function addTranslations(locale: string, bundle: Partial>): void { if (!locale) return; const existing = translations[locale]; if (existing) { Object.assign(existing, bundle); } else { translations[locale] = { ...bundle }; } } /** * Returns the list of currently registered locales (excluding English, which is always available). */ export function listLocales(): string[] { return Object.keys(translations); } // --- Initialization --- export function initI18n(pi: ExtensionAPI): () => void { try { pi.events?.emit?.("pi-core/i18n/registerBundle", { namespace, defaultLocale: "en", fallback, translations }); } catch { // Non-critical. } const unsubscribe = pi.events?.on?.("pi-core/i18n/localeChanged", (event: unknown) => { if (!event || typeof event !== "object") return; const raw = String((event as { locale?: unknown }).locale ?? "").trim(); currentLocale = raw && translations[raw] ? raw : undefined; }); try { pi.events?.emit?.("pi-core/i18n/requestApi", { namespace, onApi(api: { getLocale?: () => string | undefined }) { const raw = api.getLocale?.()?.trim(); if (raw && translations[raw]) currentLocale = raw; } }); } catch { // Non-critical. } return () => { currentLocale = undefined; unsubscribe?.(); }; } // --- Test helpers --- export function __test__resetI18n(): void { currentLocale = undefined; warnedMissing.clear(); // Clear runtime-added translations but keep built-in ones. for (const key of Object.keys(translations)) { if (!["es", "fr", "pt-BR"].includes(key)) delete translations[key]; } }