diff --git a/.changeset/fn-6311-active-agents-heartbeat-locales.md b/.changeset/fn-6311-active-agents-heartbeat-locales.md
new file mode 100644
index 0000000000..2d79c66770
--- /dev/null
+++ b/.changeset/fn-6311-active-agents-heartbeat-locales.md
@@ -0,0 +1,5 @@
+---
+"@runfusion/fusion": patch
+---
+
+Fix non-English Active Agents next-heartbeat translations so localized strings interpolate the provided elapsed heartbeat value instead of showing a raw placeholder.
diff --git a/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx b/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx
index d9951c59cc..ef08090dd7 100644
--- a/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx
@@ -1,8 +1,14 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
+import i18next from "i18next";
import { ActiveAgentsPanel } from "../ActiveAgentsPanel";
import type { Agent } from "../../api";
import { useLiveTranscript } from "../../hooks/useLiveTranscript";
+import esApp from "../../../../i18n/locales/es/app.json";
+import frApp from "../../../../i18n/locales/fr/app.json";
+import koApp from "../../../../i18n/locales/ko/app.json";
+import zhCNApp from "../../../../i18n/locales/zh-CN/app.json";
+import zhTWApp from "../../../../i18n/locales/zh-TW/app.json";
// Mock useLiveTranscript
vi.mock("../../hooks/useLiveTranscript", () => ({
@@ -14,6 +20,14 @@ vi.mock("../../hooks/useLiveTranscript", () => ({
const mockUseLiveTranscript = vi.mocked(useLiveTranscript);
+const nonEnglishAppCatalogs = [
+ ["es", esApp],
+ ["fr", frApp],
+ ["ko", koApp],
+ ["zh-CN", zhCNApp],
+ ["zh-TW", zhTWApp],
+] as const;
+
describe("ActiveAgentsPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -23,6 +37,16 @@ describe("ActiveAgentsPanel", () => {
});
});
+ afterEach(async () => {
+ await i18next.changeLanguage("en");
+ for (const [locale] of nonEnglishAppCatalogs) {
+ if (i18next.hasResourceBundle(locale, "app")) {
+ i18next.removeResourceBundle(locale, "app");
+ }
+ }
+ i18next.options.returnEmptyString = true;
+ });
+
it("renders live transcript text from entries", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [
@@ -79,6 +103,49 @@ describe("ActiveAgentsPanel", () => {
expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", undefined);
});
+ it("renders next-heartbeat labels without raw placeholders across non-English locales", async () => {
+ i18next.options.returnEmptyString = false;
+
+ for (const [locale, appCatalog] of nonEnglishAppCatalogs) {
+ i18next.addResourceBundle(locale, "app", appCatalog, true, true);
+ await i18next.changeLanguage(locale);
+
+ const futureAgent: Agent = {
+ id: `agent-future-${locale}`,
+ name: `Future Agent ${locale}`,
+ role: "executor",
+ state: "running",
+ taskId: `FN-${locale}`,
+ lastHeartbeatAt: new Date().toISOString(),
+ } as Agent;
+
+ const futureRender = render();
+ const futureBadge = futureRender.container.querySelector(".live-agent-card-next-heartbeat");
+ expect(futureBadge, `${locale} next-heartbeat badge`).toBeInTheDocument();
+ expect(futureBadge?.textContent?.trim(), `${locale} next-heartbeat text`).not.toBe("");
+ expect(futureBadge?.textContent, `${locale} next-heartbeat raw placeholder`).not.toContain("{{");
+ futureRender.unmount();
+
+ const overdueAgent: Agent = {
+ id: `agent-overdue-${locale}`,
+ name: `Overdue Agent ${locale}`,
+ role: "executor",
+ state: "running",
+ taskId: `FN-overdue-${locale}`,
+ lastHeartbeatAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
+ } as Agent;
+
+ const overdueRender = render();
+ const overdueBadge = overdueRender.container.querySelector(".live-agent-card-next-heartbeat");
+ expect(overdueBadge, `${locale} heartbeat-overdue badge`).toBeInTheDocument();
+ expect(overdueBadge?.textContent?.trim(), `${locale} heartbeat-overdue text`).not.toBe("");
+ expect(overdueBadge?.textContent, `${locale} heartbeat-overdue raw placeholder`).not.toContain("{{");
+ overdueRender.unmount();
+
+ i18next.removeResourceBundle(locale, "app");
+ }
+ });
+
it("renders empty state when no entries yet", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json
index 4471ebd341..1639325cb3 100644
--- a/packages/i18n/locales/es/app.json
+++ b/packages/i18n/locales/es/app.json
@@ -656,7 +656,7 @@
"newAgent": "Nuevo agente",
"next": "Siguiente",
"nextExpected": "Próximo esperado",
- "nextHeartbeat": "Próxima: {{time}}",
+ "nextHeartbeat": "",
"nextHeartbeatAt": "Próxima: {{time}}",
"noActiveAssignment": "Sin asignación activa",
"noActiveEligible": "Sin agentes activos elegibles para pausar",
diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json
index c8bfd722e2..9fd8329e99 100644
--- a/packages/i18n/locales/fr/app.json
+++ b/packages/i18n/locales/fr/app.json
@@ -656,7 +656,7 @@
"newAgent": "Nouvel agent",
"next": "Suivant",
"nextExpected": "Prochain attendu",
- "nextHeartbeat": "Prochaine : {{time}}",
+ "nextHeartbeat": "",
"nextHeartbeatAt": "Prochaine : {{time}}",
"noActiveAssignment": "Aucune assignation active",
"noActiveEligible": "Aucun agent actif éligible à la pause",
diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json
index 1bca616482..a033625f5d 100644
--- a/packages/i18n/locales/zh-CN/app.json
+++ b/packages/i18n/locales/zh-CN/app.json
@@ -649,7 +649,7 @@
"newAgent": "新建智能体",
"next": "下一步",
"nextExpected": "下次预期",
- "nextHeartbeat": "下次:{{time}}",
+ "nextHeartbeat": "",
"nextHeartbeatAt": "下次:{{time}}",
"noActiveAssignment": "无活动任务",
"noActiveEligible": "没有符合条件可暂停的活动代理",
diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json
index 06d1dfc7d1..7a9241c9aa 100644
--- a/packages/i18n/locales/zh-TW/app.json
+++ b/packages/i18n/locales/zh-TW/app.json
@@ -649,7 +649,7 @@
"newAgent": "新增智能體",
"next": "下一步",
"nextExpected": "下次預期",
- "nextHeartbeat": "下次:{{time}}",
+ "nextHeartbeat": "",
"nextHeartbeatAt": "下次:{{time}}",
"noActiveAssignment": "無活動任務",
"noActiveEligible": "沒有符合條件可暫停的活動代理",