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:
5
.changeset/fn-6311-active-agents-heartbeat-locales.md
Normal file
5
.changeset/fn-6311-active-agents-heartbeat-locales.md
Normal 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.
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
"newAgent": "新建智能体",
|
||||
"next": "下一步",
|
||||
"nextExpected": "下次预期",
|
||||
"nextHeartbeat": "下次:{{time}}",
|
||||
"nextHeartbeat": "",
|
||||
"nextHeartbeatAt": "下次:{{time}}",
|
||||
"noActiveAssignment": "无活动任务",
|
||||
"noActiveEligible": "没有符合条件可暂停的活动代理",
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
"newAgent": "新增智能體",
|
||||
"next": "下一步",
|
||||
"nextExpected": "下次預期",
|
||||
"nextHeartbeat": "下次:{{time}}",
|
||||
"nextHeartbeat": "",
|
||||
"nextHeartbeatAt": "下次:{{time}}",
|
||||
"noActiveAssignment": "無活動任務",
|
||||
"noActiveEligible": "沒有符合條件可暫停的活動代理",
|
||||
|
||||
Reference in New Issue
Block a user