mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:19:05 +08:00
fix(frontend): localize conversation status labels (#1010)
* fix: localize conversation status labels * refactor: update the code based on feedback * refactor: update the code based on feedback
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { StartTaskStatusBadge } from "#/components/features/conversation-panel/start-task-card/start-task-status-badge";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
describe("StartTaskStatusBadge", () => {
|
||||
// react-i18next is globally mocked so `t` echoes the key; asserting the key
|
||||
// proves the label is resolved through i18n rather than a raw enum transform.
|
||||
it.each([
|
||||
["STARTING_CONVERSATION", I18nKey.CONVERSATION$STARTING_CONVERSATION],
|
||||
["READY", I18nKey.CONVERSATION$READY],
|
||||
["ERROR", I18nKey.COMMON$ERROR],
|
||||
] as const)(
|
||||
"localizes the label for the %s status",
|
||||
(taskStatus, expectedKey) => {
|
||||
renderWithProviders(<StartTaskStatusBadge taskStatus={taskStatus} />);
|
||||
|
||||
expect(screen.getByText(expectedKey)).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it("renders nothing for the WORKING status", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<StartTaskStatusBadge taskStatus="WORKING" />,
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getStatusCode } from "#/utils/status";
|
||||
import { getStatusCode, getTaskStatusI18nKey } from "#/utils/status";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { ExecutionStatus } from "#/types/agent-server/core";
|
||||
import type { AppConversationStartTaskStatus } from "#/api/conversation-service/agent-server-conversation-service.types";
|
||||
@@ -47,3 +47,24 @@ describe("getStatusCode", () => {
|
||||
expect(result).toBe(I18nKey.CONVERSATION$STARTING_CONVERSATION);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTaskStatusI18nKey", () => {
|
||||
// Exhaustive coverage of the shared mapper over every AppConversationStartTaskStatus
|
||||
// member: the dedicated keys (WAITING_FOR_SANDBOX + the two setup keys), the
|
||||
// terminal READY/ERROR states (which now resolve to their own localized keys
|
||||
// instead of silently falling back to STARTING_CONVERSATION), and the group
|
||||
// that collapses to the generic "Starting" label (WORKING/PREPARING_REPOSITORY/
|
||||
// RUNNING_SETUP_SCRIPT). `as const` keeps the inputs typed as the union.
|
||||
it.each([
|
||||
["WAITING_FOR_SANDBOX", I18nKey.COMMON$WAITING_FOR_SANDBOX],
|
||||
["SETTING_UP_GIT_HOOKS", I18nKey.STATUS$SETTING_UP_GIT_HOOKS],
|
||||
["SETTING_UP_SKILLS", I18nKey.STATUS$SETTING_UP_SKILLS],
|
||||
["READY", I18nKey.CONVERSATION$READY],
|
||||
["ERROR", I18nKey.COMMON$ERROR],
|
||||
["WORKING", I18nKey.CONVERSATION$STARTING_CONVERSATION],
|
||||
["PREPARING_REPOSITORY", I18nKey.CONVERSATION$STARTING_CONVERSATION],
|
||||
["RUNNING_SETUP_SCRIPT", I18nKey.CONVERSATION$STARTING_CONVERSATION],
|
||||
] as const)("maps %s to its i18n key", (taskStatus, expectedKey) => {
|
||||
expect(getTaskStatusI18nKey(taskStatus)).toBe(expectedKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ const t = (key: string) => {
|
||||
COMMON$SERVER_STOPPED: "Server stopped",
|
||||
COMMON$RUNNING: "Running",
|
||||
CONVERSATION$READY: "Ready",
|
||||
CONVERSATION$STARTING_CONVERSATION: "Starting",
|
||||
CONVERSATION$ERROR_STARTING_CONVERSATION: "Error starting conversation",
|
||||
};
|
||||
return translations[key] || key;
|
||||
@@ -31,7 +32,7 @@ describe("getStatusText", () => {
|
||||
expect(result).toBe(t(I18nKey.COMMON$STOPPING));
|
||||
});
|
||||
|
||||
it("formats task status when polling a task", () => {
|
||||
it("localizes task status when polling a task", () => {
|
||||
const result = getStatusText({
|
||||
isPausing: false,
|
||||
isTask: true,
|
||||
@@ -43,7 +44,22 @@ describe("getStatusText", () => {
|
||||
t,
|
||||
});
|
||||
|
||||
expect(result).toBe("Starting conversation");
|
||||
expect(result).toBe(t(I18nKey.CONVERSATION$STARTING_CONVERSATION));
|
||||
});
|
||||
|
||||
it("prefers task detail over the localized status while polling", () => {
|
||||
const result = getStatusText({
|
||||
isPausing: false,
|
||||
isTask: true,
|
||||
taskStatus: "STARTING_CONVERSATION",
|
||||
taskDetail: "Cloning repository",
|
||||
isStartingStatus: false,
|
||||
isStopStatus: false,
|
||||
curAgentState: AgentState.RUNNING,
|
||||
t,
|
||||
});
|
||||
|
||||
expect(result).toBe("Cloning repository");
|
||||
});
|
||||
|
||||
it("returns task detail when task status is ERROR and detail exists", () => {
|
||||
|
||||
+8
-7
@@ -1,5 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AppConversationStartTaskStatus } from "#/api/conversation-service/agent-server-conversation-service.types";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { getTaskStatusI18nKey } from "#/utils/status";
|
||||
|
||||
interface StartTaskStatusBadgeProps {
|
||||
taskStatus: AppConversationStartTaskStatus;
|
||||
@@ -8,17 +10,16 @@ interface StartTaskStatusBadgeProps {
|
||||
export function StartTaskStatusBadge({
|
||||
taskStatus,
|
||||
}: StartTaskStatusBadgeProps) {
|
||||
const { t } = useTranslation("openhands");
|
||||
|
||||
// Don't show badge for WORKING status (most common, clutters UI)
|
||||
if (taskStatus === "WORKING") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format status for display
|
||||
const formatStatus = (status: string) =>
|
||||
status
|
||||
.toLowerCase()
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
// Localized status label — getTaskStatusI18nKey maps every status (including
|
||||
// the terminal READY/ERROR states) to its localized key.
|
||||
const getStatusLabel = () => t(getTaskStatusI18nKey(taskStatus));
|
||||
|
||||
// Get status color
|
||||
const getStatusStyle = () => {
|
||||
@@ -39,7 +40,7 @@ export function StartTaskStatusBadge({
|
||||
getStatusStyle(),
|
||||
)}
|
||||
>
|
||||
{formatStatus(taskStatus)}
|
||||
{getStatusLabel()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+34
-16
@@ -28,6 +28,39 @@ export function isExecutionErrored(
|
||||
return status === ExecutionStatus.ERROR || status === ExecutionStatus.STUCK;
|
||||
}
|
||||
|
||||
export function getTaskStatusI18nKey(
|
||||
taskStatus: AppConversationStartTaskStatus,
|
||||
): I18nKey {
|
||||
switch (taskStatus) {
|
||||
case "WAITING_FOR_SANDBOX":
|
||||
return I18nKey.COMMON$WAITING_FOR_SANDBOX;
|
||||
case "SETTING_UP_GIT_HOOKS":
|
||||
return I18nKey.STATUS$SETTING_UP_GIT_HOOKS;
|
||||
case "SETTING_UP_SKILLS":
|
||||
return I18nKey.STATUS$SETTING_UP_SKILLS;
|
||||
// Terminal states map to their own localized keys so any caller that
|
||||
// delegates here (now or in the future) gets a correct label instead of
|
||||
// silently falling through to STARTING_CONVERSATION. Callers that need a
|
||||
// context-specific terminal label (e.g. getStatusCode's
|
||||
// AGENT_STATUS$ERROR_OCCURRED, or getStatusText's taskDetail precedence)
|
||||
// still handle these states before delegating.
|
||||
case "READY":
|
||||
return I18nKey.CONVERSATION$READY;
|
||||
case "ERROR":
|
||||
return I18nKey.COMMON$ERROR;
|
||||
// These collapse to the generic "Starting" label. `default` is unreachable
|
||||
// for the typed union but is kept as a runtime safety net: the start-task
|
||||
// API may report a new status before this enum is updated, in which case we
|
||||
// degrade to "Starting" rather than throwing (see FUTURE_STATUS_FROM_CLOUD).
|
||||
case "STARTING_CONVERSATION":
|
||||
case "WORKING":
|
||||
case "PREPARING_REPOSITORY":
|
||||
case "RUNNING_SETUP_SCRIPT":
|
||||
default:
|
||||
return I18nKey.CONVERSATION$STARTING_CONVERSATION;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusCode(
|
||||
webSocketConnectionState: WebSocketConnectionState,
|
||||
executionStatus: ExecutionStatus | null,
|
||||
@@ -43,22 +76,7 @@ export function getStatusCode(
|
||||
}
|
||||
|
||||
if (taskStatus && taskStatus !== "READY") {
|
||||
switch (taskStatus) {
|
||||
case "WAITING_FOR_SANDBOX":
|
||||
return I18nKey.COMMON$WAITING_FOR_SANDBOX;
|
||||
case "SETTING_UP_GIT_HOOKS":
|
||||
return I18nKey.STATUS$SETTING_UP_GIT_HOOKS;
|
||||
case "SETTING_UP_SKILLS":
|
||||
return I18nKey.STATUS$SETTING_UP_SKILLS;
|
||||
case "STARTING_CONVERSATION":
|
||||
return I18nKey.CONVERSATION$STARTING_CONVERSATION;
|
||||
case "WORKING":
|
||||
case "PREPARING_REPOSITORY":
|
||||
case "RUNNING_SETUP_SCRIPT":
|
||||
return I18nKey.CONVERSATION$STARTING_CONVERSATION;
|
||||
default:
|
||||
return I18nKey.CONVERSATION$STARTING_CONVERSATION;
|
||||
}
|
||||
return getTaskStatusI18nKey(taskStatus);
|
||||
}
|
||||
|
||||
if (executionStatus === ExecutionStatus.PAUSED) {
|
||||
|
||||
+4
-8
@@ -8,6 +8,8 @@ import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { PRODUCT_URL } from "#/utils/constants";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { getTaskStatusI18nKey } from "#/utils/status";
|
||||
import type { AppConversationStartTaskStatus } from "#/api/conversation-service/agent-server-conversation-service.types";
|
||||
import {
|
||||
OH_STATUS_ERROR_COLOR,
|
||||
OH_STATUS_SUCCESS_COLOR,
|
||||
@@ -778,7 +780,7 @@ export const getStatusColor = (options: {
|
||||
interface GetStatusTextArgs {
|
||||
isPausing: boolean;
|
||||
isTask: boolean;
|
||||
taskStatus?: string | null;
|
||||
taskStatus?: AppConversationStartTaskStatus | null;
|
||||
taskDetail?: string | null;
|
||||
isStartingStatus: boolean;
|
||||
isStopStatus: boolean;
|
||||
@@ -838,13 +840,7 @@ export function getStatusText({
|
||||
return t(I18nKey.CONVERSATION$READY);
|
||||
}
|
||||
|
||||
return (
|
||||
taskDetail ||
|
||||
taskStatus
|
||||
.toLowerCase()
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase())
|
||||
);
|
||||
return taskDetail || t(getTaskStatusI18nKey(taskStatus));
|
||||
}
|
||||
|
||||
if (isStartingStatus) {
|
||||
|
||||
Reference in New Issue
Block a user