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": "沒有符合條件可暫停的活動代理",