FN-6311: fix localized next-heartbeat fallbacks

Fix Active Agents next-heartbeat localization so non-English UI falls back to interpolated elapsed text instead of raw placeholders.

- Clear stale non-English nextHeartbeat strings that referenced the obsolete {{time}} placeholder.
- Add regression coverage across supported non-English app catalogs for future and overdue heartbeat labels.
- Add a patch changeset for the published Fusion package.

Files changed:
 .../fn-6311-active-agents-heartbeat-locales.md     |  5 ++
 .../__tests__/ActiveAgentsPanel.test.tsx           | 69 +++++++++++++++++++++-
 packages/i18n/locales/es/app.json                  |  2 +-
 packages/i18n/locales/fr/app.json                  |  2 +-
 packages/i18n/locales/zh-CN/app.json               |  2 +-
 packages/i18n/locales/zh-TW/app.json               |  2 +-
 6 files changed, 77 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-6311

Fusion-Task-Lineage: b307cce4-2691-499e-bbe9-73cc1ca1705d
This commit is contained in:
gsxdsm
2026-06-12 15:34:05 -07:00
parent 2b5c4ae745
commit 480e55ffd8
6 changed files with 77 additions and 5 deletions

View File

@@ -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.

View File

@@ -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(<ActiveAgentsPanel agents={[futureAgent]} />);
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(<ActiveAgentsPanel agents={[overdueAgent]} />);
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: [],

View File

@@ -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",

View File

@@ -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",

View File

@@ -649,7 +649,7 @@
"newAgent": "新建智能体",
"next": "下一步",
"nextExpected": "下次预期",
"nextHeartbeat": "下次:{{time}}",
"nextHeartbeat": "",
"nextHeartbeatAt": "下次:{{time}}",
"noActiveAssignment": "无活动任务",
"noActiveEligible": "没有符合条件可暂停的活动代理",

View File

@@ -649,7 +649,7 @@
"newAgent": "新增智能體",
"next": "下一步",
"nextExpected": "下次預期",
"nextHeartbeat": "下次:{{time}}",
"nextHeartbeat": "",
"nextHeartbeatAt": "下次:{{time}}",
"noActiveAssignment": "無活動任務",
"noActiveEligible": "沒有符合條件可暫停的活動代理",