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 ),