diff --git a/frontend/__tests__/components/chat/chat-suggestions.test.tsx b/frontend/__tests__/components/chat/chat-suggestions.test.tsx
new file mode 100644
index 000000000000..6c781e9695a7
--- /dev/null
+++ b/frontend/__tests__/components/chat/chat-suggestions.test.tsx
@@ -0,0 +1,14 @@
+import { describe, expect, it, vi } from "vitest";
+import { screen } from "@testing-library/react";
+import { renderWithProviders } from "../../../test-utils";
+import { ChatSuggestions } from "#/components/features/chat/chat-suggestions";
+
+describe("ChatSuggestions", () => {
+ it("should display translated 'Let's start building!' text", () => {
+ const mockOnClick = vi.fn();
+ renderWithProviders();
+
+ // This will fail if the translation is missing or not properly set up
+ expect(screen.getByText("Let's start building!")).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx b/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx
index 89780e07aef7..ad5b6a0a3443 100644
--- a/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx
+++ b/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx
@@ -28,8 +28,8 @@ describe("AccountSettingsContextMenu", () => {
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
- expect(screen.getByText("ACCOUNT_SETTINGS$SETTINGS")).toBeInTheDocument();
- expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
+ expect(screen.getByText("Account Settings")).toBeInTheDocument();
+ expect(screen.getByText("Logout")).toBeInTheDocument();
});
it("should call onClickAccountSettings when the account settings option is clicked", async () => {
@@ -42,7 +42,7 @@ describe("AccountSettingsContextMenu", () => {
/>,
);
- const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
+ const accountSettingsOption = screen.getByText("Account Settings");
await user.click(accountSettingsOption);
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
@@ -58,7 +58,7 @@ describe("AccountSettingsContextMenu", () => {
/>,
);
- const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
+ const logoutOption = screen.getByText("Logout");
await user.click(logoutOption);
expect(onLogoutMock).toHaveBeenCalledOnce();
@@ -74,7 +74,7 @@ describe("AccountSettingsContextMenu", () => {
/>,
);
- const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
+ const logoutOption = screen.getByText("Logout");
await user.click(logoutOption);
expect(onLogoutMock).not.toHaveBeenCalled();
@@ -90,7 +90,7 @@ describe("AccountSettingsContextMenu", () => {
/>,
);
- const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
+ const accountSettingsButton = screen.getByText("Account Settings");
await user.click(accountSettingsButton);
await user.click(document.body);
diff --git a/frontend/__tests__/components/user-actions.test.tsx b/frontend/__tests__/components/user-actions.test.tsx
index 143af7d7113f..a83b88a38923 100644
--- a/frontend/__tests__/components/user-actions.test.tsx
+++ b/frontend/__tests__/components/user-actions.test.tsx
@@ -58,7 +58,7 @@ describe("UserActions", () => {
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
- const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
+ const accountSettingsOption = screen.getByText("Account Settings");
await user.click(accountSettingsOption);
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
@@ -79,7 +79,7 @@ describe("UserActions", () => {
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
- const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
+ const logoutOption = screen.getByText("Logout");
await user.click(logoutOption);
expect(onLogoutMock).toHaveBeenCalledOnce();
@@ -99,7 +99,7 @@ describe("UserActions", () => {
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
- const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
+ const logoutOption = screen.getByText("Logout");
await user.click(logoutOption);
expect(onLogoutMock).not.toHaveBeenCalled();
diff --git a/frontend/src/components/features/chat/chat-suggestions.tsx b/frontend/src/components/features/chat/chat-suggestions.tsx
index 794602aa2e3d..359253be74cd 100644
--- a/frontend/src/components/features/chat/chat-suggestions.tsx
+++ b/frontend/src/components/features/chat/chat-suggestions.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "react-i18next";
import { Suggestions } from "#/components/features/suggestions/suggestions";
import BuildIt from "#/icons/build-it.svg?react";
import { SUGGESTIONS } from "#/utils/suggestions";
@@ -7,12 +8,13 @@ interface ChatSuggestionsProps {
}
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
+ const { t } = useTranslation();
return (
- Let's start building!
+ {t("CHAT$LETS_START_BUILDING")}
-
{t(statusMessage)}
+
{statusMessage}
);
diff --git a/frontend/src/context/conversation-context.tsx b/frontend/src/context/conversation-context.tsx
index 86f5398b7816..748ee8ea0ac0 100644
--- a/frontend/src/context/conversation-context.tsx
+++ b/frontend/src/context/conversation-context.tsx
@@ -24,7 +24,11 @@ export function ConversationProvider({
const value = useMemo(() => ({ conversationId }), [conversationId]);
- return {children};
+ return (
+
+ {children}
+
+ );
}
export function useConversation() {
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 3707ba804b5f..216708fa39cc 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -1,4 +1,212 @@
{
+ "ACCOUNT_SETTINGS$SETTINGS": {
+ "en": "Account Settings",
+ "zh-CN": "账户设置",
+ "de": "Kontoeinstellungen",
+ "ko-KR": "계정 설정",
+ "no": "Kontoinnstillinger",
+ "zh-TW": "帳戶設定",
+ "ar": "إعدادات الحساب",
+ "fr": "Paramètres du compte",
+ "it": "Impostazioni account",
+ "pt": "Configurações da conta",
+ "es": "Ajustes de cuenta"
+ },
+ "BROWSER$EMPTY_MESSAGE": {
+ "en": "No browser content to display",
+ "zh-CN": "没有浏览器内容可显示",
+ "de": "Keine Browser-Inhalte zum Anzeigen",
+ "ko-KR": "표시할 브라우저 내용이 없습니다",
+ "no": "Ingen nettleserinnhold å vise",
+ "zh-TW": "沒有瀏覽器內容可顯示",
+ "ar": "لا يوجد محتوى متصفح للعرض",
+ "fr": "Aucun contenu de navigateur à afficher",
+ "it": "Nessun contenuto del browser da visualizzare",
+ "pt": "Nenhum conteúdo do navegador para exibir",
+ "es": "No hay contenido del navegador para mostrar"
+ },
+ "CHAT_INTERFACE$AGENT_INIT_MESSAGE": {
+ "en": "Agent is initializing...",
+ "zh-CN": "代理正在初始化...",
+ "de": "Agent wird initialisiert...",
+ "ko-KR": "에이전트 초기화 중...",
+ "no": "Agent initialiseres...",
+ "zh-TW": "代理正在初始化...",
+ "ar": "جاري تهيئة الوكيل...",
+ "fr": "L'agent est en cours d'initialisation...",
+ "it": "L'agente si sta inizializzando...",
+ "pt": "O agente está inicializando...",
+ "es": "El agente se está inicializando..."
+ },
+ "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE": {
+ "en": "Agent is running...",
+ "zh-CN": "代理正在运行...",
+ "de": "Agent läuft...",
+ "ko-KR": "에이전트 실행 중...",
+ "no": "Agent kjører...",
+ "zh-TW": "代理正在運行...",
+ "ar": "الوكيل قيد التشغيل...",
+ "fr": "L'agent est en cours d'exécution...",
+ "it": "L'agente è in esecuzione...",
+ "pt": "O agente está em execução...",
+ "es": "El agente está en ejecución..."
+ },
+ "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE": {
+ "en": "Waiting for user input...",
+ "zh-CN": "等待用户输入...",
+ "de": "Warte auf Benutzereingabe...",
+ "ko-KR": "사용자 입력 대기 중...",
+ "no": "Venter på brukerinndata...",
+ "zh-TW": "等待使用者輸入...",
+ "ar": "في انتظار إدخال المستخدم...",
+ "fr": "En attente de la saisie de l'utilisateur...",
+ "it": "In attesa dell'input dell'utente...",
+ "pt": "Aguardando entrada do usuário...",
+ "es": "Esperando entrada del usuario..."
+ },
+ "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": {
+ "en": "Agent is paused",
+ "zh-CN": "代理已暂停",
+ "de": "Agent ist pausiert",
+ "ko-KR": "에이전트가 일시 중지됨",
+ "no": "Agent er pauset",
+ "zh-TW": "代理已暫停",
+ "ar": "الوكيل متوقف مؤقتاً",
+ "fr": "L'agent est en pause",
+ "it": "L'agente è in pausa",
+ "pt": "O agente está pausado",
+ "es": "El agente está pausado"
+ },
+ "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
+ "en": "Loading...",
+ "zh-CN": "加载中...",
+ "de": "Lädt...",
+ "ko-KR": "로딩 중...",
+ "no": "Laster...",
+ "zh-TW": "載入中...",
+ "ar": "جاري التحميل...",
+ "fr": "Chargement...",
+ "it": "Caricamento...",
+ "pt": "Carregando...",
+ "es": "Cargando..."
+ },
+ "CHAT_INTERFACE$AGENT_STOPPED_MESSAGE": {
+ "en": "Agent has stopped",
+ "zh-CN": "代理已停止",
+ "de": "Agent wurde gestoppt",
+ "ko-KR": "에이전트가 중지됨",
+ "no": "Agent har stoppet",
+ "zh-TW": "代理已停止",
+ "ar": "توقف الوكيل",
+ "fr": "L'agent s'est arrêté",
+ "it": "L'agente si è fermato",
+ "pt": "O agente parou",
+ "es": "El agente se ha detenido"
+ },
+ "CHAT_INTERFACE$AGENT_FINISHED_MESSAGE": {
+ "en": "Agent has finished",
+ "zh-CN": "代理已完成",
+ "de": "Agent ist fertig",
+ "ko-KR": "에이전트가 완료됨",
+ "no": "Agent er ferdig",
+ "zh-TW": "代理已完成",
+ "ar": "انتهى الوكيل",
+ "fr": "L'agent a terminé",
+ "it": "L'agente ha terminato",
+ "pt": "O agente terminou",
+ "es": "El agente ha terminado"
+ },
+ "CHAT_INTERFACE$AGENT_REJECTED_MESSAGE": {
+ "en": "Agent was rejected",
+ "zh-CN": "代理被拒绝",
+ "de": "Agent wurde abgelehnt",
+ "ko-KR": "에이전트가 거부됨",
+ "no": "Agent ble avvist",
+ "zh-TW": "代理被拒絕",
+ "ar": "تم رفض الوكيل",
+ "fr": "L'agent a été rejeté",
+ "it": "L'agente è stato rifiutato",
+ "pt": "O agente foi rejeitado",
+ "es": "El agente fue rechazado"
+ },
+ "CHAT_INTERFACE$AGENT_ERROR_MESSAGE": {
+ "en": "An error occurred",
+ "zh-CN": "发生错误",
+ "de": "Ein Fehler ist aufgetreten",
+ "ko-KR": "오류가 발생했습니다",
+ "no": "En feil oppstod",
+ "zh-TW": "發生錯誤",
+ "ar": "حدث خطأ",
+ "fr": "Une erreur s'est produite",
+ "it": "Si è verificato un errore",
+ "pt": "Ocorreu um erro",
+ "es": "Se produjo un error"
+ },
+ "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": {
+ "en": "Waiting for user confirmation...",
+ "zh-CN": "等待用户确认...",
+ "de": "Warte auf Benutzerbestätigung...",
+ "ko-KR": "사용자 확인 대기 중...",
+ "no": "Venter på brukerbekreftelse...",
+ "zh-TW": "等待使用者確認...",
+ "ar": "في انتظار تأكيد المستخدم...",
+ "fr": "En attente de la confirmation de l'utilisateur...",
+ "it": "In attesa della conferma dell'utente...",
+ "pt": "Aguardando confirmação do usuário...",
+ "es": "Esperando confirmación del usuario..."
+ },
+ "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": {
+ "en": "User confirmed",
+ "zh-CN": "用户已确认",
+ "de": "Benutzer hat bestätigt",
+ "ko-KR": "사용자가 확인함",
+ "no": "Bruker bekreftet",
+ "zh-TW": "使用者已確認",
+ "ar": "تم تأكيد المستخدم",
+ "fr": "Utilisateur confirmé",
+ "it": "Utente confermato",
+ "pt": "Usuário confirmou",
+ "es": "Usuario confirmado"
+ },
+ "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": {
+ "en": "User rejected",
+ "zh-CN": "用户已拒绝",
+ "de": "Benutzer hat abgelehnt",
+ "ko-KR": "사용자가 거부함",
+ "no": "Bruker avvist",
+ "zh-TW": "使用者已拒絕",
+ "ar": "تم رفض المستخدم",
+ "fr": "Utilisateur rejeté",
+ "it": "Utente rifiutato",
+ "pt": "Usuário rejeitou",
+ "es": "Usuario rechazado"
+ },
+ "CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE": {
+ "en": "Rate limited, please wait...",
+ "zh-CN": "已达到速率限制,请稍候...",
+ "de": "Rate limitiert, bitte warten...",
+ "ko-KR": "속도 제한됨, 기다려 주세요...",
+ "no": "Frekvensbegrenset, vennligst vent...",
+ "zh-TW": "已達到速率限制,請稍候...",
+ "ar": "تم تقييد المعدل، يرجى الانتظار...",
+ "fr": "Taux limité, veuillez patienter...",
+ "it": "Limite di velocità raggiunto, attendere prego...",
+ "pt": "Taxa limitada, por favor aguarde...",
+ "es": "Límite de velocidad alcanzado, por favor espere..."
+ },
+ "CHAT$LETS_START_BUILDING": {
+ "en": "Let's start building!",
+ "zh-CN": "让我们开始构建!",
+ "de": "Lass uns anfangen zu bauen!",
+ "ko-KR": "시작해 봅시다!",
+ "no": "La oss begynne å bygge!",
+ "zh-TW": "讓我們開始構建!",
+ "ar": "دعنا نبدأ البناء!",
+ "fr": "Commençons à construire !",
+ "it": "Iniziamo a costruire!",
+ "pt": "Vamos começar a construir!",
+ "es": "¡Empecemos a construir!"
+ },
"WORKSPACE$TITLE": {
"en": "OpenHands Workspace",
"zh-CN": "OpenHands 工作区",
@@ -2035,13 +2243,30 @@
"en": "Pause the current task"
},
"BROWSER$SCREENSHOT_ALT": {
- "en": "Browser Screenshot"
- },
- "ACCOUNT_SETTINGS$SETTINGS": {
- "en": "Account Settings"
+ "en": "Browser Screenshot",
+ "zh-CN": "浏览器截图",
+ "de": "Browser-Screenshot",
+ "ko-KR": "브라우저 스크린샷",
+ "no": "Nettleser skjermbilde",
+ "zh-TW": "瀏覽器截圖",
+ "ar": "لقطة شاشة المتصفح",
+ "fr": "Capture d'écran du navigateur",
+ "it": "Screenshot del browser",
+ "pt": "Captura de tela do navegador",
+ "es": "Captura de pantalla del navegador"
},
"ACCOUNT_SETTINGS$LOGOUT": {
- "en": "Logout"
+ "en": "Logout",
+ "zh-CN": "退出登录",
+ "de": "Abmelden",
+ "ko-KR": "로그아웃",
+ "no": "Logg ut",
+ "zh-TW": "登出",
+ "ar": "تسجيل الخروج",
+ "fr": "Déconnexion",
+ "it": "Disconnetti",
+ "pt": "Sair",
+ "es": "Cerrar sesión"
},
"ERROR_TOAST$CLOSE_BUTTON_LABEL": {
"en": "Close"
diff --git a/frontend/test-utils.tsx b/frontend/test-utils.tsx
index 8eeb5e453fb6..66fe8841c177 100644
--- a/frontend/test-utils.tsx
+++ b/frontend/test-utils.tsx
@@ -31,7 +31,60 @@ i18n.use(initReactI18next).init({
defaultNS: "translation",
resources: {
en: {
- translation: {},
+ translation: {
+ ACCOUNT_SETTINGS$SETTINGS: "Account Settings",
+ ACCOUNT_SETTINGS$LOGOUT: "Logout",
+ BROWSER$EMPTY_MESSAGE: "No browser content to display",
+ CHAT_INTERFACE$AGENT_INIT_MESSAGE: "Agent is initializing...",
+ CHAT_INTERFACE$AGENT_RUNNING_MESSAGE: "Agent is running...",
+ CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE: "Waiting for user input...",
+ CHAT_INTERFACE$AGENT_PAUSED_MESSAGE: "Agent is paused",
+ CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE: "Loading...",
+ CHAT_INTERFACE$AGENT_STOPPED_MESSAGE: "Agent has stopped",
+ CHAT_INTERFACE$AGENT_FINISHED_MESSAGE: "Agent has finished",
+ CHAT_INTERFACE$AGENT_REJECTED_MESSAGE: "Agent was rejected",
+ CHAT_INTERFACE$AGENT_ERROR_MESSAGE: "An error occurred",
+ CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE: "Waiting for user confirmation...",
+ CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE: "User confirmed",
+ CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE: "User rejected",
+ CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE: "Rate limited, please wait...",
+ CHAT$LETS_START_BUILDING: "Let's start building!",
+ STATUS$STARTING_RUNTIME: "Starting Runtime...",
+ STATUS$STARTING_CONTAINER: "Preparing container, this might take a few minutes...",
+ STATUS$PREPARING_CONTAINER: "Preparing to start container...",
+ STATUS$CONTAINER_STARTED: "Container started.",
+ STATUS$WAITING_FOR_CLIENT: "Waiting for client to become ready...",
+ STATUS$ERROR_LLM_AUTHENTICATION: "Error authenticating with the LLM provider. Please check your API key",
+ STATUS$ERROR_RUNTIME_DISCONNECTED: "There was an error while connecting to the runtime. Please refresh the page.",
+ AGENT_ERROR$BAD_ACTION: "Agent tried to execute a malformed action.",
+ AGENT_ERROR$ACTION_TIMEOUT: "Action timed out.",
+ WORKSPACE$TITLE: "OpenHands Workspace",
+ WORKSPACE$TERMINAL_TAB_LABEL: "Terminal",
+ WORKSPACE$PLANNER_TAB_LABEL: "Planner",
+ WORKSPACE$JUPYTER_TAB_LABEL: "Jupyter IPython",
+ WORKSPACE$CODE_EDITOR_TAB_LABEL: "Code Editor",
+ WORKSPACE$BROWSER_TAB_LABEL: "Browser (Experimental)",
+ BROWSER$SCREENSHOT_ALT: "Browser Screenshot",
+ ERROR_TOAST$CLOSE_BUTTON_LABEL: "Close",
+ FILE_EXPLORER$UPLOAD: "Upload File",
+ FILE_EXPLORER$REFRESH_WORKSPACE: "Refresh workspace",
+ FILE_EXPLORER$OPEN_WORKSPACE: "Open workspace",
+ FILE_EXPLORER$CLOSE_WORKSPACE: "Close workspace",
+ ACTION_MESSAGE$RUN: "Running a bash command",
+ ACTION_MESSAGE$RUN_IPYTHON: "Running a Python command",
+ ACTION_MESSAGE$READ: "Reading the contents of a file",
+ ACTION_MESSAGE$EDIT: "Editing the contents of a file",
+ ACTION_MESSAGE$WRITE: "Writing to a file",
+ ACTION_MESSAGE$BROWSE: "Browsing the web",
+ OBSERVATION_MESSAGE$RUN: "Ran a bash command",
+ OBSERVATION_MESSAGE$RUN_IPYTHON: "Ran a Python command",
+ OBSERVATION_MESSAGE$READ: "Read the contents of a file",
+ OBSERVATION_MESSAGE$EDIT: "Edited the contents of a file",
+ OBSERVATION_MESSAGE$WRITE: "Wrote to a file",
+ OBSERVATION_MESSAGE$BROWSE: "Browsing completed",
+ EXPANDABLE_MESSAGE$SHOW_DETAILS: "Show details",
+ EXPANDABLE_MESSAGE$HIDE_DETAILS: "Hide details"
+ },
},
},
interpolation: {
diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts
index e9a89c8677f6..8628d902b926 100644
--- a/frontend/vitest.setup.ts
+++ b/frontend/vitest.setup.ts
@@ -10,16 +10,29 @@ HTMLCanvasElement.prototype.getContext = vi.fn();
HTMLElement.prototype.scrollTo = vi.fn();
// Mock the i18n provider
-vi.mock("react-i18next", async (importOriginal) => ({
- ...(await importOriginal()),
- useTranslation: () => ({
- t: (key: string) => key,
- i18n: {
- language: "en",
- exists: () => false,
- },
- }),
-}));
+vi.mock("react-i18next", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useTranslation: () => ({
+ t: (key: string) => {
+ // Load the actual translations
+ const translations = require("./src/i18n/translation.json");
+ if (!translations[key] || !translations[key]["en"]) {
+ throw new Error(`Missing translation for key: ${key}`);
+ }
+ return translations[key]["en"];
+ },
+ i18n: {
+ language: "en",
+ exists: (key: string) => {
+ const translations = require("./src/i18n/translation.json");
+ return !!translations[key];
+ },
+ },
+ }),
+ };
+});
// Mock requests during tests
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
diff --git a/openhands/runtime/impl/docker/docker_runtime.py b/openhands/runtime/impl/docker/docker_runtime.py
index 852e50f617d1..f5962943b2c0 100644
--- a/openhands/runtime/impl/docker/docker_runtime.py
+++ b/openhands/runtime/impl/docker/docker_runtime.py
@@ -267,10 +267,7 @@ def _init_container(self):
environment=environment,
volumes=volumes,
device_requests=(
- [docker.types.DeviceRequest(
- capabilities=[['gpu']],
- count=-1
- )]
+ [docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)]
if self.config.sandbox.enable_gpu
else None
),