Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

type Locale = "es" | "fr" | "pt-BR";
type Key = keyof typeof fallback;
type Params = Record<string, string | number>;

const namespace = "aliou/pi-processes";

const fallback = {
"kill.missingId": "Missing required parameter: id",
"kill.notFound": "Process not found: {id}",
"kill.terminated": 'Terminated "{name}" ({id})',
"kill.timeout":
'SIGTERM timed out for "{name}" ({id}). Run /ps and press x on terminate_timeout to force kill (SIGKILL).',
"kill.failed": 'Failed to terminate "{name}" ({id})',
"list.none": "No background processes running",
"list.summary": "{count} process(es):\n{summary}",
"list.header": "{count} process(es), {running} running/terminating",
"list.field.processes": "Processes",
"list.pid": "pid",
"list.status": "status",
"list.started": "started",
"list.ended": "ended",
"list.runtime": "runtime",
"list.noRunning": "no running process",
"list.finished": "{count} finished",
"list.failed": "{count} failed",
"list.killed": "{count} killed",
"footer.running": "running",
"footer.failed": "failed",
"footer.killed": "killed",
} as const;

const translations: Record<Locale, Partial<Record<Key, string>>> = {
es: {
"kill.missingId": "Falta el parámetro requerido: id",
"kill.notFound": "Proceso no encontrado: {id}",
"kill.terminated": 'Proceso terminado "{name}" ({id})',
"kill.timeout":
'SIGTERM agotó el tiempo para "{name}" ({id}). Ejecuta /ps y pulsa x en terminate_timeout para forzar la finalización (SIGKILL).',
"kill.failed": 'No se pudo terminar "{name}" ({id})',
"list.none": "No hay procesos en segundo plano en ejecución",
"list.summary": "{count} proceso(s):\n{summary}",
"list.header": "{count} proceso(s), {running} en ejecución/terminando",
"list.field.processes": "Procesos",
"list.pid": "pid",
"list.status": "estado",
"list.started": "iniciado",
"list.ended": "finalizado",
"list.runtime": "duración",
"list.noRunning": "ningún proceso en ejecución",
"list.finished": "{count} finalizados",
"list.failed": "{count} fallidos",
"list.killed": "{count} terminados",
"footer.running": "en ejecución",
"footer.failed": "fallidos",
"footer.killed": "terminados",
},
fr: {
"kill.missingId": "Paramètre requis manquant : id",
"kill.notFound": "Processus introuvable : {id}",
"kill.terminated": 'Processus terminé "{name}" ({id})',
"kill.timeout":
'SIGTERM a expiré pour "{name}" ({id}). Exécutez /ps et appuyez sur x sur terminate_timeout pour forcer l’arrêt (SIGKILL).',
"kill.failed": 'Impossible de terminer "{name}" ({id})',
"list.none": "Aucun processus en arrière-plan en cours",
"list.summary": "{count} processus :\n{summary}",
"list.header": "{count} processus, {running} en cours/en arrêt",
"list.field.processes": "Processus",
"list.pid": "pid",
"list.status": "état",
"list.started": "démarré",
"list.ended": "terminé",
"list.runtime": "durée",
"list.noRunning": "aucun processus en cours",
"list.finished": "{count} terminés",
"list.failed": "{count} échoués",
"list.killed": "{count} tués",
"footer.running": "en cours",
"footer.failed": "échoués",
"footer.killed": "tués",
},
"pt-BR": {
"kill.missingId": "Parâmetro obrigatório ausente: id",
"kill.notFound": "Processo não encontrado: {id}",
"kill.terminated": 'Processo encerrado "{name}" ({id})',
"kill.timeout":
'SIGTERM esgotou o tempo para "{name}" ({id}). Execute /ps e pressione x em terminate_timeout para forçar o encerramento (SIGKILL).',
"kill.failed": 'Falha ao encerrar "{name}" ({id})',
"list.none": "Nenhum processo em segundo plano em execução",
"list.summary": "{count} processo(s):\n{summary}",
"list.header": "{count} processo(s), {running} em execução/encerrando",
"list.field.processes": "Processos",
"list.pid": "pid",
"list.status": "status",
"list.started": "iniciado",
"list.ended": "finalizado",
"list.runtime": "duração",
"list.noRunning": "nenhum processo em execução",
"list.finished": "{count} finalizados",
"list.failed": "{count} com falha",
"list.killed": "{count} encerrados",
"footer.running": "em execução",
"footer.failed": "com falha",
"footer.killed": "encerrados",
},
};

let currentLocale: string | undefined;

function format(template: string, params: Params = {}): string {
return template.replace(/\{(\w+)\}/g, (_match, key) =>
String(params[key] ?? `{${key}}`),
);
}

export function t(key: Key, params?: Params): string {
const locale = currentLocale as Locale | undefined;
const template = locale ? translations[locale]?.[key] : undefined;
return format(template ?? fallback[key], params);
}

export function initI18n(pi: ExtensionAPI): void {
pi.events?.emit?.("pi-core/i18n/registerBundle", {
namespace,
defaultLocale: "en",
fallback,
translations,
});
pi.events?.on?.("pi-core/i18n/localeChanged", (event: unknown) => {
currentLocale =
event && typeof event === "object" && "locale" in event
? String((event as { locale?: unknown }).locale ?? "")
: undefined;
});
pi.events?.emit?.("pi-core/i18n/requestApi", {
namespace,
onApi(api: { getLocale?: () => string | undefined }) {
currentLocale = api.getLocale?.();
},
});
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { setupProcessesCommands } from "./commands";
import { registerProcessesSettings } from "./commands/settings";
import { configLoader } from "./config";
import { setupProcessesHooks } from "./hooks";
import { initI18n } from "./i18n";
import { ProcessManager } from "./manager";
import { setupProcessesTools } from "./tools";

export default async function (pi: ExtensionAPI) {
initI18n(pi);

if (process.platform === "win32") {
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
Expand Down
15 changes: 7 additions & 8 deletions src/tools/actions/kill.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ToolCallHeader } from "@aliou/pi-utils-ui";
import type { Theme } from "@mariozechner/pi-coding-agent";
import type { ExecuteResult } from "../../constants";
import { t } from "../../i18n";
import type { ProcessManager } from "../../manager";

interface KillParams {
Expand All @@ -24,18 +25,18 @@ export async function executeKill(
): Promise<ExecuteResult> {
if (!params.id) {
return {
content: [{ type: "text", text: "Missing required parameter: id" }],
content: [{ type: "text", text: t("kill.missingId") }],
details: {
action: "kill",
success: false,
message: "Missing required parameter: id",
message: t("kill.missingId"),
},
};
}

const proc = manager.get(params.id);
if (!proc) {
const message = `Process not found: ${params.id}`;
const message = t("kill.notFound", { id: params.id });
return {
content: [{ type: "text", text: message }],
details: {
Expand All @@ -52,7 +53,7 @@ export async function executeKill(
});

if (result.ok) {
const message = `Terminated "${proc.name}" (${proc.id})`;
const message = t("kill.terminated", { name: proc.name, id: proc.id });
return {
content: [{ type: "text", text: message }],
details: {
Expand All @@ -64,9 +65,7 @@ export async function executeKill(
}

if (result.reason === "timeout") {
const message =
`SIGTERM timed out for "${proc.name}" (${proc.id}). ` +
"Run /ps and press x on terminate_timeout to force kill (SIGKILL).";
const message = t("kill.timeout", { name: proc.name, id: proc.id });
return {
content: [{ type: "text", text: message }],
details: {
Expand All @@ -77,7 +76,7 @@ export async function executeKill(
};
}

const message = `Failed to terminate "${proc.name}" (${proc.id})`;
const message = t("kill.failed", { name: proc.name, id: proc.id });
return {
content: [{ type: "text", text: message }],
details: {
Expand Down
40 changes: 22 additions & 18 deletions src/tools/actions/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import type { ExecuteResult, ProcessesDetails } from "../../constants";
import { t } from "../../i18n";
import type { ProcessManager } from "../../manager";
import {
formatRuntime,
Expand All @@ -20,11 +21,11 @@ export function executeList(manager: ProcessManager): ExecuteResult {

if (processes.length === 0) {
return {
content: [{ type: "text", text: "No background processes running" }],
content: [{ type: "text", text: t("list.none") }],
details: {
action: "list",
success: true,
message: "No background processes running",
message: t("list.none"),
processes: [],
},
};
Expand All @@ -37,7 +38,7 @@ export function executeList(manager: ProcessManager): ExecuteResult {
)
.join("\n");

const message = `${processes.length} process(es):\n${summary}`;
const message = t("list.summary", { count: processes.length, summary });
return {
content: [{ type: "text", text: message }],
details: {
Expand All @@ -61,8 +62,8 @@ export function renderListResult(
{
fields: [
{
label: "Processes",
value: "No background processes running",
label: t("list.field.processes"),
value: t("list.none"),
showCollapsed: true,
},
],
Expand Down Expand Up @@ -104,7 +105,7 @@ export function renderListResult(
const lines: string[] = [
theme.fg(
"success",
`${processes.length} process(es), ${runningCount} running/terminating`,
t("list.header", { count: processes.length, running: runningCount }),
),
];

Expand All @@ -113,10 +114,10 @@ export function renderListResult(
lines.push(
[
`- ${theme.fg("accent", process.name)} ${theme.fg("muted", `(${process.id})`)}`,
` pid: ${process.pid} status: ${status}`,
` started: ${theme.fg("muted", formatTimestamp(process.startTime))}`,
` ended: ${theme.fg("muted", formatTimestamp(process.endTime))}`,
` runtime: ${theme.fg("muted", formatRuntime(process.startTime, process.endTime))}`,
` ${t("list.pid")}: ${process.pid} ${t("list.status")}: ${status}`,
` ${t("list.started")}: ${theme.fg("muted", formatTimestamp(process.startTime))}`,
` ${t("list.ended")}: ${theme.fg("muted", formatTimestamp(process.endTime))}`,
` ${t("list.runtime")}: ${theme.fg("muted", formatRuntime(process.startTime, process.endTime))}`,
].join("\n"),
);
}
Expand Down Expand Up @@ -145,17 +146,17 @@ export function renderListResult(
`${theme.fg("accent", `"${p.name}"`)} [${formatStatusTag(p, theme)}]`,
)
.join(", ")
: theme.fg("muted", "no running process");
: theme.fg("muted", t("list.noRunning"));

const restParts: string[] = [];
if (finishedOk > 0) restParts.push(`${finishedOk} finished`);
if (failed > 0) restParts.push(`${failed} failed`);
if (killed > 0) restParts.push(`${killed} killed`);
if (finishedOk > 0) restParts.push(t("list.finished", { count: finishedOk }));
if (failed > 0) restParts.push(t("list.failed", { count: failed }));
if (killed > 0) restParts.push(t("list.killed", { count: killed }));
const restSummary =
restParts.length > 0 ? theme.fg("muted", ` + ${restParts.join(", ")}`) : "";

fields.push({
label: "Processes",
label: t("list.field.processes"),
value: runningSummary + restSummary,
showCollapsed: true,
});
Expand All @@ -165,13 +166,16 @@ export function renderListResult(
value: string;
}> = [];
if (runningCount > 0) {
footerItems.push({ label: "running", value: String(runningCount) });
footerItems.push({
label: t("footer.running"),
value: String(runningCount),
});
}
if (failed > 0) {
footerItems.push({ label: "failed", value: String(failed) });
footerItems.push({ label: t("footer.failed"), value: String(failed) });
}
if (killed > 0) {
footerItems.push({ label: "killed", value: String(killed) });
footerItems.push({ label: t("footer.killed"), value: String(killed) });
}

return new ToolBody(
Expand Down