From fe4c0569f7ec8ea2cfb87dfee892e6be9f5d2840 Mon Sep 17 00:00:00 2001 From: Chris Bagwell Date: Wed, 18 Mar 2026 09:57:23 -0500 Subject: [PATCH 01/21] Remove unused WORK_HOSTS_SKILL_FOOTER (#12594) --- .../app_server/app_conversation/skill_loader.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openhands/app_server/app_conversation/skill_loader.py b/openhands/app_server/app_conversation/skill_loader.py index 3638c6e681..d5976aedac 100644 --- a/openhands/app_server/app_conversation/skill_loader.py +++ b/openhands/app_server/app_conversation/skill_loader.py @@ -33,21 +33,6 @@ class ExposedUrlConfig(BaseModel): port: int -WORK_HOSTS_SKILL_FOOTER = """ -When starting a web server, use the corresponding ports via environment variables: -- $WORKER_1 for the first port -- $WORKER_2 for the second port - -**CRITICAL: You MUST enable CORS and bind to 0.0.0.0.** Without CORS headers, the App tab cannot detect your server and will show an empty state. - -Example (Flask): -```python -from flask_cors import CORS -CORS(app) -app.run(host='0.0.0.0', port=int(os.environ.get('WORKER_1', 12000))) -```""" - - class SandboxConfig(BaseModel): """Sandbox configuration for agent-server API request.""" From 6589e592e371a9320abd9f8da1c1c8dbde3078ab Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:50:16 +0700 Subject: [PATCH 02/21] feat(frontend): add contextual info messages on LLM settings page (org project) (#13460) --- .../__tests__/routes/llm-settings.test.tsx | 194 +++++++++++++++++- frontend/src/i18n/declaration.ts | 2 + frontend/src/i18n/translation.json | 32 +++ frontend/src/routes/llm-settings.tsx | 32 +++ 4 files changed, 259 insertions(+), 1 deletion(-) diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index 2dabd4da79..615ad9396c 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -13,7 +13,39 @@ import * as ToastHandlers from "#/utils/custom-toast-handlers"; import OptionService from "#/api/option-service/option-service.api"; import { organizationService } from "#/api/organization-service/organization-service.api"; import { useSelectedOrganizationStore } from "#/stores/selected-organization-store"; -import type { OrganizationMember } from "#/types/org"; +import type { Organization, OrganizationMember } from "#/types/org"; + +/** Creates a mock Organization with default values for testing */ +const createMockOrganization = ( + overrides: Partial & Pick, +): Organization => ({ + contact_name: "", + contact_email: "", + conversation_expiration: 0, + agent: "CodeActAgent", + default_max_iterations: 20, + security_analyzer: "", + confirmation_mode: false, + default_llm_model: "", + default_llm_api_key_for_byor: "", + default_llm_base_url: "", + remote_runtime_resource_factor: 1, + enable_default_condenser: true, + billing_margin: 0, + enable_proactive_conversation_starters: false, + sandbox_base_container_image: "", + sandbox_runtime_container_image: "", + org_version: 1, + mcp_config: { tools: [], settings: {} }, + search_api_key: null, + sandbox_api_key: null, + max_budget_per_task: 0, + enable_solvability_analysis: false, + v1_enabled: true, + credits: 0, + is_personal: false, + ...overrides, +}); // Mock react-router hooks const mockUseSearchParams = vi.fn(); @@ -1767,3 +1799,163 @@ describe("clientLoader permission checks", () => { expect(typeof clientLoader).toBe("function"); }); }); + +describe("Contextual info messages", () => { + it("should show admin message when user is an admin in a team organization", async () => { + // Arrange + const orgId = "team-org-1"; + const adminMeData: OrganizationMember = { + org_id: orgId, + user_id: "1", + email: "admin@example.com", + role: "admin", + status: "active", + llm_api_key: "", + max_iterations: 20, + llm_model: "", + llm_api_key_for_byor: null, + llm_base_url: "", + }; + + mockUseConfig.mockReturnValue({ + data: { app_mode: "saas" }, + isLoading: false, + }); + + vi.spyOn(organizationService, "getMe").mockResolvedValue(adminMeData); + vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({ + items: [ + createMockOrganization({ + id: orgId, + name: "Team Org", + is_personal: false, + }), + ], + currentOrgId: orgId, + }); + + // Act + renderLlmSettingsScreen(orgId, adminMeData); + + // Assert + await waitFor(() => { + expect( + screen.getByTestId("llm-settings-info-message"), + ).toBeInTheDocument(); + }); + + expect(screen.getByTestId("llm-settings-info-message")).toHaveTextContent( + "SETTINGS$LLM_ADMIN_INFO", + ); + }); + + it("should show member message when user is a member in a team organization", async () => { + // Arrange + const orgId = "team-org-2"; + const memberMeData: OrganizationMember = { + org_id: orgId, + user_id: "2", + email: "member@example.com", + role: "member", + status: "active", + llm_api_key: "", + max_iterations: 20, + llm_model: "", + llm_api_key_for_byor: null, + llm_base_url: "", + }; + + mockUseConfig.mockReturnValue({ + data: { app_mode: "saas" }, + isLoading: false, + }); + + vi.spyOn(organizationService, "getMe").mockResolvedValue(memberMeData); + vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({ + items: [ + createMockOrganization({ + id: orgId, + name: "Team Org", + is_personal: false, + }), + ], + currentOrgId: orgId, + }); + + // Act + renderLlmSettingsScreen(orgId, memberMeData); + + // Assert + await waitFor(() => { + expect( + screen.getByTestId("llm-settings-info-message"), + ).toBeInTheDocument(); + }); + + expect(screen.getByTestId("llm-settings-info-message")).toHaveTextContent( + "SETTINGS$LLM_MEMBER_INFO", + ); + }); + + it("should not show info message in personal workspace", async () => { + // Arrange + const orgId = "personal-org-1"; + const ownerMeData: OrganizationMember = { + org_id: orgId, + user_id: "3", + email: "user@example.com", + role: "owner", + status: "active", + llm_api_key: "", + max_iterations: 20, + llm_model: "", + llm_api_key_for_byor: null, + llm_base_url: "", + }; + + mockUseConfig.mockReturnValue({ + data: { app_mode: "saas" }, + isLoading: false, + }); + + vi.spyOn(organizationService, "getMe").mockResolvedValue(ownerMeData); + vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({ + items: [ + createMockOrganization({ id: orgId, name: "Personal", is_personal: true }), + ], + currentOrgId: orgId, + }); + + // Act + renderLlmSettingsScreen(orgId, ownerMeData); + + // Assert + await waitFor(() => { + expect(screen.getByTestId("llm-settings-screen")).toBeInTheDocument(); + }); + + expect( + screen.queryByTestId("llm-settings-info-message"), + ).not.toBeInTheDocument(); + }); + + it("should not show info message in OSS mode", async () => { + // Arrange + mockUseConfig.mockReturnValue({ + data: { app_mode: "oss" }, + isLoading: false, + }); + + // Act + renderLlmSettingsScreen(); + + // Assert + await waitFor(() => { + expect(screen.getByTestId("llm-settings-screen")).toBeInTheDocument(); + }); + + expect( + screen.queryByTestId("llm-settings-info-message"), + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index 94f72e7c95..a66552ff3c 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -434,6 +434,8 @@ export enum I18nKey { SETTINGS$OPENHANDS_API_KEY_HELP_TEXT = "SETTINGS$OPENHANDS_API_KEY_HELP_TEXT", SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX = "SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX", SETTINGS$LLM_BILLING_INFO = "SETTINGS$LLM_BILLING_INFO", + SETTINGS$LLM_ADMIN_INFO = "SETTINGS$LLM_ADMIN_INFO", + SETTINGS$LLM_MEMBER_INFO = "SETTINGS$LLM_MEMBER_INFO", SETTINGS$SEE_PRICING_DETAILS = "SETTINGS$SEE_PRICING_DETAILS", SETTINGS$CREATE_API_KEY = "SETTINGS$CREATE_API_KEY", SETTINGS$CREATE_API_KEY_DESCRIPTION = "SETTINGS$CREATE_API_KEY_DESCRIPTION", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index c712b2a2e1..515a7e11b5 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -6943,6 +6943,38 @@ "de": "LLM-Nutzung wird zu Anbieterpreisen ohne Aufschlag abgerechnet.", "uk": "Використання LLM оплачується за тарифами провайдерів без надбавки." }, + "SETTINGS$LLM_ADMIN_INFO": { + "en": "The LLM settings configured below will apply to all users of the organization.", + "ja": "以下に設定されたLLM設定は、組織のすべてのユーザーに適用されます。", + "zh-CN": "以下配置的LLM设置将应用于组织的所有用户。", + "zh-TW": "以下配置的LLM設定將適用於組織的所有用戶。", + "ko-KR": "아래에 구성된 LLM 설정은 조직의 모든 사용자에게 적용됩니다.", + "no": "LLM-innstillingene som er konfigurert nedenfor, vil gjelde for alle brukere i organisasjonen.", + "it": "Le impostazioni LLM configurate di seguito si applicheranno a tutti gli utenti dell'organizzazione.", + "pt": "As configurações de LLM configuradas abaixo serão aplicadas a todos os usuários da organização.", + "es": "La configuración de LLM configurada a continuación se aplicará a todos los usuarios de la organización.", + "ar": "ستنطبق إعدادات LLM المكونة أدناه على جميع مستخدمي المؤسسة.", + "fr": "Les paramètres LLM configurés ci-dessous s'appliqueront à tous les utilisateurs de l'organisation.", + "tr": "Aşağıda yapılandırılan LLM ayarları, kuruluştaki tüm kullanıcılara uygulanacaktır.", + "de": "Die unten konfigurierten LLM-Einstellungen gelten für alle Benutzer der Organisation.", + "uk": "Налаштування LLM, налаштовані нижче, будуть застосовані до всіх користувачів організації." + }, + "SETTINGS$LLM_MEMBER_INFO": { + "en": "LLM settings are managed by your organization's administrator.", + "ja": "LLM設定は組織の管理者によって管理されています。", + "zh-CN": "LLM设置由您的组织管理员管理。", + "zh-TW": "LLM設定由您的組織管理員管理。", + "ko-KR": "LLM 설정은 조직 관리자가 관리합니다.", + "no": "LLM-innstillinger administreres av organisasjonens administrator.", + "it": "Le impostazioni LLM sono gestite dall'amministratore della tua organizzazione.", + "pt": "As configurações de LLM são gerenciadas pelo administrador da sua organização.", + "es": "La configuración de LLM es administrada por el administrador de su organización.", + "ar": "يتم إدارة إعدادات LLM بواسطة مسؤول مؤسستك.", + "fr": "Les paramètres LLM sont gérés par l'administrateur de votre organisation.", + "tr": "LLM ayarları kuruluşunuzun yöneticisi tarafından yönetilmektedir.", + "de": "LLM-Einstellungen werden vom Administrator Ihrer Organisation verwaltet.", + "uk": "Налаштування LLM керуються адміністратором вашої організації." + }, "SETTINGS$SEE_PRICING_DETAILS": { "en": "See pricing details.", "ja": "価格詳細を見る。", diff --git a/frontend/src/routes/llm-settings.tsx b/frontend/src/routes/llm-settings.tsx index d9489ec35a..b5c394c064 100644 --- a/frontend/src/routes/llm-settings.tsx +++ b/frontend/src/routes/llm-settings.tsx @@ -31,6 +31,7 @@ import { getProviderId } from "#/utils/map-provider"; import { DEFAULT_OPENHANDS_MODEL } from "#/utils/verified-models"; import { useMe } from "#/hooks/query/use-me"; import { usePermission } from "#/hooks/organizations/use-permissions"; +import { useOrgTypeAndAccess } from "#/hooks/use-org-type-and-access"; interface OpenHandsApiKeyHelpProps { testId: string; @@ -74,12 +75,35 @@ function LlmSettingsScreen() { const { data: config } = useConfig(); const { data: me } = useMe(); const { hasPermission } = usePermission(me?.role ?? "member"); + const { isPersonalOrg, isTeamOrg } = useOrgTypeAndAccess(); // In OSS mode, user has full access (no permission restrictions) // In SaaS mode, check role-based permissions (members can only view, owners and admins can edit) const isOssMode = config?.app_mode === "oss"; const isReadOnly = isOssMode ? false : !hasPermission("edit_llm_settings"); + // Determine the contextual info message based on workspace type and role + const getLlmSettingsInfoMessage = (): I18nKey | null => { + // No message in OSS mode (no organization context) + if (isOssMode) return null; + + // No message for personal workspaces + if (isPersonalOrg) return null; + + // Team org - show appropriate message based on role + if (isTeamOrg) { + const role = me?.role ?? "member"; + if (role === "admin" || role === "owner") { + return I18nKey.SETTINGS$LLM_ADMIN_INFO; + } + return I18nKey.SETTINGS$LLM_MEMBER_INFO; + } + + return null; + }; + + const llmInfoMessage = getLlmSettingsInfoMessage(); + const [view, setView] = React.useState<"basic" | "advanced">("basic"); const [dirtyInputs, setDirtyInputs] = React.useState({ @@ -504,6 +528,14 @@ function LlmSettingsScreen() { className="flex flex-col h-full justify-between" >
+ {llmInfoMessage && ( +

+ {t(llmInfoMessage)} +

+ )} Date: Wed, 18 Mar 2026 22:50:30 +0700 Subject: [PATCH 03/21] feat(frontend): improve conversation access error message with workspace hint (org project) (#13461) --- frontend/src/i18n/translation.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 515a7e11b5..93ec856439 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -16096,20 +16096,20 @@ "uk": "Не вдалося запустити розмову із завдання." }, "CONVERSATION$NOT_EXIST_OR_NO_PERMISSION": { - "en": "This conversation does not exist, or you do not have permission to access it.", - "ja": "この会話は存在しないか、アクセスする権限がありません。", - "zh-CN": "此对话不存在,或您没有访问权限。", - "zh-TW": "此對話不存在,或您沒有訪問權限。", - "ko-KR": "이 대화가 존재하지 않거나 액세스 권한이 없습니다.", - "no": "Denne samtalen eksisterer ikke, eller du har ikke tillatelse til å få tilgang til den.", - "it": "Questa conversazione non esiste o non hai il permesso di accedervi.", - "pt": "Esta conversa não existe ou você não tem permissão para acessá-la.", - "es": "Esta conversación no existe o no tienes permiso para acceder a ella.", - "ar": "هذه المحادثة غير موجودة أو ليس لديك إذن للوصول إليها.", - "fr": "Cette conversation n'existe pas ou vous n'avez pas la permission d'y accéder.", - "tr": "Bu konuşma mevcut değil veya erişim izniniz yok.", - "de": "Diese Konversation existiert nicht oder Sie haben keine Berechtigung darauf zuzugreifen.", - "uk": "Ця розмова не існує або у вас немає дозволу на доступ до неї." + "en": "This conversation does not exist, or you do not have permission to access it. If this is your conversation, try switching to the workspace where it was created.", + "ja": "この会話は存在しないか、アクセスする権限がありません。あなたの会話であれば、作成したワークスペースに切り替えてみてください。", + "zh-CN": "此对话不存在,或您没有访问权限。如果这是您的对话,请尝试切换到创建该对话的工作区。", + "zh-TW": "此對話不存在,或您沒有訪問權限。如果這是您的對話,請嘗試切換到創建該對話的工作區。", + "ko-KR": "이 대화가 존재하지 않거나 액세스 권한이 없습니다. 본인의 대화인 경우, 대화가 생성된 워크스페이스로 전환해 보세요.", + "no": "Denne samtalen eksisterer ikke, eller du har ikke tillatelse til å få tilgang til den. Hvis dette er din samtale, prøv å bytte til arbeidsområdet der den ble opprettet.", + "it": "Questa conversazione non esiste o non hai il permesso di accedervi. Se è una tua conversazione, prova a passare all'area di lavoro in cui è stata creata.", + "pt": "Esta conversa não existe ou você não tem permissão para acessá-la. Se esta é sua conversa, tente mudar para o workspace onde ela foi criada.", + "es": "Esta conversación no existe o no tienes permiso para acceder a ella. Si es tu conversación, intenta cambiar al espacio de trabajo donde fue creada.", + "ar": "هذه المحادثة غير موجودة أو ليس لديك إذن للوصول إليها. إذا كانت هذه محادثتك، حاول التبديل إلى مساحة العمل التي تم إنشاؤها فيها.", + "fr": "Cette conversation n'existe pas ou vous n'avez pas la permission d'y accéder. S'il s'agit de votre conversation, essayez de passer à l'espace de travail où elle a été créée.", + "tr": "Bu konuşma mevcut değil veya erişim izniniz yok. Bu sizin konuşmanızsa, oluşturulduğu çalışma alanına geçmeyi deneyin.", + "de": "Diese Konversation existiert nicht oder Sie haben keine Berechtigung darauf zuzugreifen. Wenn dies Ihre Konversation ist, versuchen Sie zum Arbeitsbereich zu wechseln, in dem sie erstellt wurde.", + "uk": "Ця розмова не існує або у вас немає дозволу на доступ до неї. Якщо це ваша розмова, спробуйте перейти до робочої області, де вона була створена." }, "CONVERSATION$FAILED_TO_START_WITH_ERROR": { "en": "Failed to start conversation: {{error}}", From 5d1f9f815ac35421bad541e4dc56cdab67979b92 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:50:42 +0700 Subject: [PATCH 04/21] fix(frontend): preserve settings page route on browser refresh (org project) (#13462) --- .../__tests__/utils/permission-checks.test.ts | 238 ++++++++++++++---- frontend/src/types/org.ts | 9 + frontend/src/utils/org/permission-checks.ts | 41 ++- 3 files changed, 241 insertions(+), 47 deletions(-) diff --git a/frontend/__tests__/utils/permission-checks.test.ts b/frontend/__tests__/utils/permission-checks.test.ts index d4730cb2a7..db5a09951e 100644 --- a/frontend/__tests__/utils/permission-checks.test.ts +++ b/frontend/__tests__/utils/permission-checks.test.ts @@ -1,10 +1,18 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { PermissionKey } from "#/utils/org/permissions"; +import { OrganizationMember, OrganizationsQueryData } from "#/types/org"; +import { + getAvailableRolesAUserCanAssign, + getActiveOrganizationUser, +} from "#/utils/org/permission-checks"; +import { getSelectedOrganizationIdFromStore } from "#/stores/selected-organization-store"; +import { queryClient } from "#/query-client-config"; -// Mock dependencies for getActiveOrganizationUser tests +// Mock dependencies vi.mock("#/api/organization-service/organization-service.api", () => ({ organizationService: { getMe: vi.fn(), + getOrganizations: vi.fn(), }, })); @@ -12,49 +20,60 @@ vi.mock("#/stores/selected-organization-store", () => ({ getSelectedOrganizationIdFromStore: vi.fn(), })); -vi.mock("#/utils/query-client-getters", () => ({ - getMeFromQueryClient: vi.fn(), -})); - vi.mock("#/query-client-config", () => ({ queryClient: { + getQueryData: vi.fn(), + fetchQuery: vi.fn(), setQueryData: vi.fn(), }, })); -// Import after mocks are set up -import { - getAvailableRolesAUserCanAssign, - getActiveOrganizationUser, -} from "#/utils/org/permission-checks"; -import { organizationService } from "#/api/organization-service/organization-service.api"; -import { getSelectedOrganizationIdFromStore } from "#/stores/selected-organization-store"; -import { getMeFromQueryClient } from "#/utils/query-client-getters"; +// Test fixtures +const mockUser: OrganizationMember = { + org_id: "org-1", + user_id: "user-1", + email: "test@example.com", + role: "admin", + llm_api_key: "", + max_iterations: 100, + llm_model: "gpt-4", + llm_api_key_for_byor: null, + llm_base_url: "", + status: "active", +}; + +const mockOrganizationsData: OrganizationsQueryData = { + items: [ + { id: "org-1", name: "Org 1" }, + { id: "org-2", name: "Org 2" }, + ] as OrganizationsQueryData["items"], + currentOrgId: "org-1", +}; describe("getAvailableRolesAUserCanAssign", () => { - it("returns empty array if user has no permissions", () => { - const result = getAvailableRolesAUserCanAssign([]); - expect(result).toEqual([]); - }); + it("returns empty array if user has no permissions", () => { + const result = getAvailableRolesAUserCanAssign([]); + expect(result).toEqual([]); + }); - it("returns only roles the user has permission for", () => { - const userPermissions: PermissionKey[] = [ - "change_user_role:member", - "change_user_role:admin", - ]; - const result = getAvailableRolesAUserCanAssign(userPermissions); - expect(result.sort()).toEqual(["admin", "member"].sort()); - }); + it("returns only roles the user has permission for", () => { + const userPermissions: PermissionKey[] = [ + "change_user_role:member", + "change_user_role:admin", + ]; + const result = getAvailableRolesAUserCanAssign(userPermissions); + expect(result.sort()).toEqual(["admin", "member"].sort()); + }); - it("returns all roles if user has all permissions", () => { - const allPermissions: PermissionKey[] = [ - "change_user_role:member", - "change_user_role:admin", - "change_user_role:owner", - ]; - const result = getAvailableRolesAUserCanAssign(allPermissions); - expect(result.sort()).toEqual(["member", "admin", "owner"].sort()); - }); + it("returns all roles if user has all permissions", () => { + const allPermissions: PermissionKey[] = [ + "change_user_role:member", + "change_user_role:admin", + "change_user_role:owner", + ]; + const result = getAvailableRolesAUserCanAssign(allPermissions); + expect(result.sort()).toEqual(["member", "admin", "owner"].sort()); + }); }); describe("getActiveOrganizationUser", () => { @@ -62,18 +81,147 @@ describe("getActiveOrganizationUser", () => { vi.clearAllMocks(); }); - it("should return undefined when API call throws an error", async () => { - // Arrange: orgId exists, cache is empty, API call fails - vi.mocked(getSelectedOrganizationIdFromStore).mockReturnValue("org-1"); - vi.mocked(getMeFromQueryClient).mockReturnValue(undefined); - vi.mocked(organizationService.getMe).mockRejectedValue( - new Error("Network error"), - ); + describe("when orgId exists in store", () => { + it("should fetch user directly using stored orgId", async () => { + // Arrange + vi.mocked(getSelectedOrganizationIdFromStore).mockReturnValue("org-1"); + vi.mocked(queryClient.fetchQuery).mockResolvedValue(mockUser); - // Act - const result = await getActiveOrganizationUser(); + // Act + const result = await getActiveOrganizationUser(); - // Assert: should return undefined instead of propagating the error - expect(result).toBeUndefined(); + // Assert + expect(result).toEqual(mockUser); + expect(queryClient.getQueryData).not.toHaveBeenCalled(); + expect(queryClient.fetchQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["organizations", "org-1", "me"], + }), + ); + }); + + it("should return undefined when user fetch fails", async () => { + // Arrange + vi.mocked(getSelectedOrganizationIdFromStore).mockReturnValue("org-1"); + vi.mocked(queryClient.fetchQuery).mockRejectedValue( + new Error("Network error"), + ); + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe("when orgId is null in store (page refresh scenario)", () => { + beforeEach(() => { + vi.mocked(getSelectedOrganizationIdFromStore).mockReturnValue(null); + }); + + it("should use currentOrgId from cached organizations data", async () => { + // Arrange + vi.mocked(queryClient.getQueryData).mockReturnValue( + mockOrganizationsData, + ); + vi.mocked(queryClient.fetchQuery).mockResolvedValue(mockUser); + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toEqual(mockUser); + expect(queryClient.getQueryData).toHaveBeenCalledWith(["organizations"]); + expect(queryClient.fetchQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["organizations", "org-1", "me"], + }), + ); + }); + + it("should fallback to first org when currentOrgId is null", async () => { + // Arrange + const dataWithoutCurrentOrg: OrganizationsQueryData = { + items: [ + { id: "first-org" }, + { id: "second-org" }, + ] as OrganizationsQueryData["items"], + currentOrgId: null, + }; + vi.mocked(queryClient.getQueryData).mockReturnValue( + dataWithoutCurrentOrg, + ); + vi.mocked(queryClient.fetchQuery).mockResolvedValue(mockUser); + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toEqual(mockUser); + expect(queryClient.fetchQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ["organizations", "first-org", "me"], + }), + ); + }); + + it("should fetch organizations when not in cache", async () => { + // Arrange + vi.mocked(queryClient.getQueryData).mockReturnValue(undefined); + vi.mocked(queryClient.fetchQuery) + .mockResolvedValueOnce(mockOrganizationsData) // First call: fetch organizations + .mockResolvedValueOnce(mockUser); // Second call: fetch user + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toEqual(mockUser); + expect(queryClient.fetchQuery).toHaveBeenCalledTimes(2); + expect(queryClient.fetchQuery).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + queryKey: ["organizations"], + }), + ); + expect(queryClient.fetchQuery).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + queryKey: ["organizations", "org-1", "me"], + }), + ); + }); + + it("should return undefined when fetching organizations fails", async () => { + // Arrange + vi.mocked(queryClient.getQueryData).mockReturnValue(undefined); + vi.mocked(queryClient.fetchQuery).mockRejectedValue( + new Error("Failed to fetch organizations"), + ); + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toBeUndefined(); + }); + + it("should return undefined when no organizations exist", async () => { + // Arrange + const emptyData: OrganizationsQueryData = { + items: [], + currentOrgId: null, + }; + vi.mocked(queryClient.getQueryData).mockReturnValue(emptyData); + + // Act + const result = await getActiveOrganizationUser(); + + // Assert + expect(result).toBeUndefined(); + // Should not attempt to fetch user since there's no orgId + expect(queryClient.fetchQuery).not.toHaveBeenCalled(); + }); }); }); diff --git a/frontend/src/types/org.ts b/frontend/src/types/org.ts index a527d49c47..a7b83c1092 100644 --- a/frontend/src/types/org.ts +++ b/frontend/src/types/org.ts @@ -56,3 +56,12 @@ export interface OrganizationMembersPage { export type UpdateOrganizationMemberParams = Partial< Omit >; + +/** + * Query data structure for the organizations query. + * This represents the raw data returned by queryClient before any `select` transform. + */ +export type OrganizationsQueryData = { + items: Organization[]; + currentOrgId: string | null; +}; diff --git a/frontend/src/utils/org/permission-checks.ts b/frontend/src/utils/org/permission-checks.ts index 5bf9db857e..4b439af6d8 100644 --- a/frontend/src/utils/org/permission-checks.ts +++ b/frontend/src/utils/org/permission-checks.ts @@ -1,6 +1,10 @@ import { organizationService } from "#/api/organization-service/organization-service.api"; import { getSelectedOrganizationIdFromStore } from "#/stores/selected-organization-store"; -import { OrganizationMember, OrganizationUserRole } from "#/types/org"; +import { + OrganizationMember, + OrganizationsQueryData, + OrganizationUserRole, +} from "#/types/org"; import { PermissionKey } from "./permissions"; import { queryClient } from "#/query-client-config"; @@ -8,12 +12,45 @@ import { queryClient } from "#/query-client-config"; * Get the active organization user. * Uses React Query's fetchQuery to leverage request deduplication, * preventing duplicate API calls when multiple consumers request the same data. + * + * On page refresh, the Zustand store resets and orgId becomes null. + * In this case, we retrieve the organization from the query cache or fetch it + * from the backend to ensure permission checks work correctly. + * * @returns OrganizationMember */ export const getActiveOrganizationUser = async (): Promise< OrganizationMember | undefined > => { - const orgId = getSelectedOrganizationIdFromStore(); + let orgId = getSelectedOrganizationIdFromStore(); + + // If no orgId in store (e.g., after page refresh), try to get it from query cache or fetch + if (!orgId) { + // Check if organizations data is already in the cache + let organizationsData = queryClient.getQueryData([ + "organizations", + ]); + + // If not in cache, fetch it + if (!organizationsData) { + try { + organizationsData = await queryClient.fetchQuery({ + queryKey: ["organizations"], + queryFn: organizationService.getOrganizations, + staleTime: 1000 * 60 * 5, // 5 minutes - matches useOrganizations hook + }); + } catch { + return undefined; + } + } + + // Use currentOrgId from backend (user's last selected org) or first org as fallback + orgId = + organizationsData?.currentOrgId ?? + organizationsData?.items?.[0]?.id ?? + null; + } + if (!orgId) return undefined; try { From 35a40ddee87427c1fa3f156be42965ef5b61a840 Mon Sep 17 00:00:00 2001 From: Chris Bagwell Date: Wed, 18 Mar 2026 10:55:48 -0500 Subject: [PATCH 05/21] fix: handle containers with tagless images in DockerSandboxService (#13238) --- .../sandbox/docker_sandbox_service.py | 6 ++ .../app_server/test_docker_sandbox_service.py | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/openhands/app_server/sandbox/docker_sandbox_service.py b/openhands/app_server/sandbox/docker_sandbox_service.py index cb1466a8fa..6c692a680a 100644 --- a/openhands/app_server/sandbox/docker_sandbox_service.py +++ b/openhands/app_server/sandbox/docker_sandbox_service.py @@ -197,6 +197,12 @@ class DockerSandboxService(SandboxService): ) ) + if not container.image.tags: + _logger.debug( + f'Skipping container {container.name!r}: image has no tags (image id: {container.image.id})' + ) + return None + return SandboxInfo( id=container.name, created_by_user_id=None, diff --git a/tests/unit/app_server/test_docker_sandbox_service.py b/tests/unit/app_server/test_docker_sandbox_service.py index b951167d02..23a6d51b04 100644 --- a/tests/unit/app_server/test_docker_sandbox_service.py +++ b/tests/unit/app_server/test_docker_sandbox_service.py @@ -245,6 +245,61 @@ class TestDockerSandboxService: assert len(result.items) == 0 assert result.next_page_id is None + async def test_search_sandboxes_skips_containers_with_no_image_tags( + self, service, mock_running_container + ): + """Test that containers with tagless images are skipped without crashing. + + Regression test: when a container's image has been rebuilt with the same tag, + the old container's image loses its tags, causing container.image.tags to be + an empty list. Previously this caused an IndexError. + """ + # Setup a container with no image tags (e.g. image was retagged/rebuilt) + tagless_container = MagicMock() + tagless_container.name = 'oh-test-tagless' + tagless_container.status = 'paused' + tagless_container.image.tags = [] + tagless_container.image.id = 'sha256:abc123def456' + tagless_container.attrs = { + 'Created': '2024-01-15T10:30:00.000000000Z', + 'Config': {'Env': []}, + 'NetworkSettings': {'Ports': {}}, + } + + service.docker_client.containers.list.return_value = [ + mock_running_container, + tagless_container, + ] + service.httpx_client.get.return_value.raise_for_status.return_value = None + + # Execute - should not raise IndexError + result = await service.search_sandboxes() + + # Verify - only the properly tagged container is returned + assert isinstance(result, SandboxPage) + assert len(result.items) == 1 + assert result.items[0].id == 'oh-test-abc123' + + async def test_get_sandbox_returns_none_for_tagless_image(self, service): + """Test that get_sandbox returns None for containers with tagless images.""" + tagless_container = MagicMock() + tagless_container.name = 'oh-test-tagless' + tagless_container.status = 'paused' + tagless_container.image.tags = [] + tagless_container.image.id = 'sha256:abc123def456' + tagless_container.attrs = { + 'Created': '2024-01-15T10:30:00.000000000Z', + 'Config': {'Env': []}, + 'NetworkSettings': {'Ports': {}}, + } + service.docker_client.containers.get.return_value = tagless_container + + # Execute - should not raise IndexError + result = await service.get_sandbox('oh-test-tagless') + + # Verify - returns None for tagless container + assert result is None + async def test_search_sandboxes_filters_by_prefix(self, service): """Test that search filters containers by name prefix.""" # Setup From 991585c05d995e5a6d89308fbef2e9d8aea7de7c Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Wed, 18 Mar 2026 16:00:23 +0000 Subject: [PATCH 06/21] =?UTF-8?q?docs:=20add=20cross-repo=20testing=20skil?= =?UTF-8?q?l=20for=20SDK=20=E2=86=94=20OH=20Cloud=20e2e=20workflow=20(#134?= =?UTF-8?q?46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: openhands --- .agents/skills/cross-repo-testing/SKILL.md | 202 +++++++++++++++++++++ .github/workflows/pr-artifacts.yml | 136 ++++++++++++++ AGENTS.md | 34 ++++ 3 files changed, 372 insertions(+) create mode 100644 .agents/skills/cross-repo-testing/SKILL.md create mode 100644 .github/workflows/pr-artifacts.yml diff --git a/.agents/skills/cross-repo-testing/SKILL.md b/.agents/skills/cross-repo-testing/SKILL.md new file mode 100644 index 0000000000..3ca98ac58d --- /dev/null +++ b/.agents/skills/cross-repo-testing/SKILL.md @@ -0,0 +1,202 @@ +--- +name: cross-repo-testing +description: This skill should be used when the user asks to "test a cross-repo feature", "deploy a feature branch to staging", "test SDK against OH Cloud", "e2e test a cloud workspace feature", "test provider tokens", "test secrets inheritance", or when changes span the SDK and OpenHands server repos and need end-to-end validation against a staging deployment. +triggers: +- cross-repo +- staging deployment +- feature branch deploy +- test against cloud +- e2e cloud +--- + +# Cross-Repo Testing: SDK ↔ OpenHands Cloud + +How to end-to-end test features that span `OpenHands/software-agent-sdk` and `OpenHands/OpenHands` (the Cloud backend). + +## Repository Map + +| Repo | Role | What lives here | +|------|------|-----------------| +| [`software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) | Agent core | `openhands-sdk`, `openhands-workspace`, `openhands-tools` packages. `OpenHandsCloudWorkspace` lives here. | +| [`OpenHands`](https://github.com/OpenHands/OpenHands) | Cloud backend | FastAPI server (`openhands/app_server/`), sandbox management, auth, enterprise integrations. Deployed as OH Cloud. | +| [`deploy`](https://github.com/OpenHands/deploy) | Infrastructure | Helm charts + GitHub Actions that build the enterprise Docker image and deploy to staging/production. | + +**Data flow:** SDK client → OH Cloud API (`/api/v1/...`) → sandbox agent-server (inside runtime container) + +## When You Need This + +There are **two flows** depending on which direction the dependency goes: + +| Flow | When | Example | +|------|------|---------| +| **A — SDK client → new Cloud API** | The SDK calls an API that doesn't exist yet on production | `workspace.get_llm()` calling `GET /api/v1/users/me?expose_secrets=true` | +| **B — OH server → new SDK code** | The Cloud server needs unreleased SDK packages or a new agent-server image | Server consumes a new tool, agent behavior, or workspace method from the SDK | + +Flow A only requires deploying the server PR. Flow B requires pinning the SDK to an unreleased commit in the server PR **and** using the SDK PR's agent-server image. Both flows may apply simultaneously. + +--- + +## Flow A: SDK Client Tests Against New Cloud API + +Use this when the SDK calls an endpoint that only exists on the server PR branch. + +### A1. Write and test the server-side changes + +In the `OpenHands` repo, implement the new API endpoint(s). Run unit tests: + +```bash +cd OpenHands +poetry run pytest tests/unit/app_server/test_.py -v +``` + +Push a PR. Wait for the **"Push Enterprise Image" (Docker) CI job** to succeed — this builds `ghcr.io/openhands/enterprise-server:sha-`. + +### A2. Write the SDK-side changes + +In `software-agent-sdk`, implement the client code (e.g., new methods on `OpenHandsCloudWorkspace`). Run SDK unit tests: + +```bash +cd software-agent-sdk +pip install -e openhands-sdk -e openhands-workspace +pytest tests/ -v +``` + +Push a PR. SDK CI is independent — it doesn't need the server changes to pass unit tests. + +### A3. Deploy the server PR to staging + +See [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) below. + +### A4. Run the SDK e2e test against staging + +See [Running E2E Tests Against Staging](#running-e2e-tests-against-staging) below. + +--- + +## Flow B: OH Server Needs Unreleased SDK Code + +Use this when the Cloud server depends on SDK changes that haven't been released to PyPI yet. The server's runtime containers run the `agent-server` image built from the SDK repo, so the server PR must be configured to use the SDK PR's image and packages. + +### B1. Get the SDK PR merged (or identify the commit) + +The SDK PR must have CI pass so its agent-server Docker image is built. The image is tagged with the **merge-commit SHA** from GitHub Actions — NOT the head-commit SHA shown in the PR. + +Find the correct image tag: +- Check the SDK PR description for an `AGENT_SERVER_IMAGES` section +- Or check the "Consolidate Build Information" CI job for `"short_sha": ""` + +### B2. Pin SDK packages to the commit in the OpenHands PR + +In the `OpenHands` repo PR, pin all 3 SDK packages (`openhands-sdk`, `openhands-agent-server`, `openhands-tools`) to the unreleased commit and update the agent-server image tag. This involves editing 3 files and regenerating 3 lock files. + +Follow the **`update-sdk` skill** → "Development: Pin SDK to an Unreleased Commit" section for the full procedure and file-by-file instructions. + +### B3. Wait for the OpenHands enterprise image to build + +Push the pinned changes. The OpenHands CI will build a new enterprise Docker image (`ghcr.io/openhands/enterprise-server:sha-`) that bundles the unreleased SDK. Wait for the "Push Enterprise Image" job to succeed. + +### B4. Deploy and test + +Follow [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) using the new OpenHands commit SHA. + +### B5. Before merging: remove the pin + +**CI guard:** `check-package-versions.yml` blocks merge to `main` if `[tool.poetry.dependencies]` contains `rev` fields. Before the OpenHands PR can merge, the SDK PR must be merged and released to PyPI, then the pin must be replaced with the released version number. + +--- + +## Deploying to a Staging Feature Environment + +The `deploy` repo creates preview environments from OpenHands PRs. + +**Option A — GitHub Actions UI (preferred):** +Go to `OpenHands/deploy` → Actions → "Create OpenHands preview PR" → enter the OpenHands PR number. This creates a branch `ohpr--` and opens a deploy PR. + +**Option B — Update an existing feature branch:** +```bash +cd deploy +git checkout ohpr-- +# In .github/workflows/deploy.yaml, update BOTH: +# OPENHANDS_SHA: "" +# OPENHANDS_RUNTIME_IMAGE_TAG: "-nikolaik" +git commit -am "Update OPENHANDS_SHA to " && git push +``` + +**Before updating the SHA**, verify the enterprise Docker image exists: +```bash +gh api repos/OpenHands/OpenHands/actions/runs \ + --jq '.workflow_runs[] | select(.head_sha=="") | "\(.name): \(.conclusion)"' \ + | grep Docker +# Must show: "Docker: success" +``` + +The deploy CI auto-triggers and creates the environment at: +``` +https://ohpr--.staging.all-hands.dev +``` + +**Wait for it to be live:** +```bash +curl -s -o /dev/null -w "%{http_code}" https://ohpr--.staging.all-hands.dev/api/v1/health +# 401 = server is up (auth required). DNS may take 1-2 min on first deploy. +``` + +## Running E2E Tests Against Staging + +**Critical: Feature deployments have their own Keycloak instance.** API keys from `app.all-hands.dev` or `$OPENHANDS_API_KEY` will NOT work. You need a test API key issued by the specific feature deployment's Keycloak. + +**You (the agent) cannot obtain this key yourself** — the feature environment requires interactive browser login with credentials you do not have. You must **ask the user** to: +1. Log in to the feature deployment at `https://ohpr--.staging.all-hands.dev` in their browser +2. Generate a test API key from the UI +3. Provide the key to you so you can proceed with e2e testing + +Do **not** attempt to log in via the browser or guess credentials. Wait for the user to supply the key before running any e2e tests. + +```python +from openhands.workspace import OpenHandsCloudWorkspace + +STAGING = "https://ohpr--.staging.all-hands.dev" + +with OpenHandsCloudWorkspace( + cloud_api_url=STAGING, + cloud_api_key="", +) as workspace: + # Test the new feature + llm = workspace.get_llm() + secrets = workspace.get_secrets() + print(f"LLM: {llm.model}, secrets: {list(secrets.keys())}") +``` + +Or run an example script: +```bash +OPENHANDS_CLOUD_API_KEY="" \ +OPENHANDS_CLOUD_API_URL="https://ohpr--.staging.all-hands.dev" \ +python examples/02_remote_agent_server/10_cloud_workspace_saas_credentials.py +``` + +### Recording results + +Both repos support a `.pr/` directory for temporary PR artifacts (design docs, test logs, scripts). These files are automatically removed when the PR is approved — see `.github/workflows/pr-artifacts.yml` and the "PR-Specific Artifacts" section in each repo's `AGENTS.md`. + +Push test output to the `.pr/logs/` directory of whichever repo you're working in: +```bash +mkdir -p .pr/logs +python test_script.py 2>&1 | tee .pr/logs/.log +git add -f .pr/logs/ +git commit -m "docs: add e2e test results" && git push +``` + +Comment on **both PRs** with pass/fail summary and link to logs. + +## Key Gotchas + +| Gotcha | Details | +|--------|---------| +| **Feature env auth is isolated** | Each `ohpr-*` deployment has its own Keycloak. Production API keys don't work. Agents cannot log in — you must ask the user to provide a test API key from the feature deployment's UI. | +| **Two SHAs in deploy.yaml** | `OPENHANDS_SHA` and `OPENHANDS_RUNTIME_IMAGE_TAG` must both be updated. The runtime tag is `-nikolaik`. | +| **Enterprise image must exist** | The Docker CI job on the OpenHands PR must succeed before you can deploy. If it hasn't run, push an empty commit to trigger it. | +| **DNS propagation** | First deployment of a new branch takes 1-2 min for DNS. Subsequent deploys are instant. | +| **Merge-commit SHA ≠ head SHA** | SDK CI tags Docker images with GitHub Actions' merge-commit SHA, not the PR head SHA. Check the SDK PR description or CI logs for the correct tag. | +| **SDK pin blocks merge** | `check-package-versions.yml` prevents merging an OpenHands PR that has `rev` fields in `[tool.poetry.dependencies]`. The SDK must be released to PyPI first. | +| **Flow A: stock agent-server is fine** | When only the Cloud API changes, `OpenHandsCloudWorkspace` talks to the Cloud server, not the agent-server. No custom image needed. | +| **Flow B: agent-server image is required** | When the server needs new SDK code inside runtime containers, you must pin to the SDK PR's agent-server image. | diff --git a/.github/workflows/pr-artifacts.yml b/.github/workflows/pr-artifacts.yml new file mode 100644 index 0000000000..ef656c1119 --- /dev/null +++ b/.github/workflows/pr-artifacts.yml @@ -0,0 +1,136 @@ +--- +name: PR Artifacts + +on: + workflow_dispatch: # Manual trigger for testing + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + pull_request_review: + types: [submitted] + +jobs: + # Auto-remove .pr/ directory when a reviewer approves + cleanup-on-approval: + concurrency: + group: cleanup-pr-artifacts-${{ github.event.pull_request.number }} + cancel-in-progress: false + if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Check if fork PR + id: check-fork + run: | + if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.event.pull_request.base.repo.full_name }}" ]; then + echo "is_fork=true" >> $GITHUB_OUTPUT + echo "::notice::Fork PR detected - skipping auto-cleanup (manual removal required)" + else + echo "is_fork=false" >> $GITHUB_OUTPUT + fi + + - uses: actions/checkout@v5 + if: steps.check-fork.outputs.is_fork == 'false' + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }} + + - name: Remove .pr/ directory + id: remove + if: steps.check-fork.outputs.is_fork == 'false' + run: | + if [ -d ".pr" ]; then + git config user.name "allhands-bot" + git config user.email "allhands-bot@users.noreply.github.com" + git rm -rf .pr/ + git commit -m "chore: Remove PR-only artifacts [automated]" + git push || { + echo "::error::Failed to push cleanup commit. Check branch protection rules." + exit 1 + } + echo "removed=true" >> $GITHUB_OUTPUT + echo "::notice::Removed .pr/ directory" + else + echo "removed=false" >> $GITHUB_OUTPUT + echo "::notice::No .pr/ directory to remove" + fi + + - name: Update PR comment after cleanup + if: steps.check-fork.outputs.is_fork == 'false' && steps.remove.outputs.removed == 'true' + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const body = `${marker} + ✅ **PR Artifacts Cleaned Up** + + The \`.pr/\` directory has been automatically removed. + `; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + } + + # Warn if .pr/ directory exists (will be auto-removed on approval) + check-pr-artifacts: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v5 + + - name: Check for .pr/ directory + id: check + run: | + if [ -d ".pr" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "::warning::.pr/ directory exists and will be automatically removed when the PR is approved. For fork PRs, manual removal is required before merging." + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Post or update PR comment + if: steps.check.outputs.exists == 'true' + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const body = `${marker} + 📁 **PR Artifacts Notice** + + This PR contains a \`.pr/\` directory with PR-specific documents. This directory will be **automatically removed** when the PR is approved. + + > For fork PRs: Manual removal is required before merging. + `; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (!existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + } diff --git a/AGENTS.md b/AGENTS.md index 7a0bbc044a..811f3bdcf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,40 @@ then re-run the command to ensure it passes. Common issues include: - Be especially careful with `git reset --hard` after staging files, as it will remove accidentally staged files - When remote has new changes, use `git fetch upstream && git rebase upstream/` on the same branch +## PR-Specific Artifacts (`.pr/` directory) + +When working on a PR that requires design documents, scripts meant for development-only, or other temporary artifacts that should NOT be merged to main, store them in a `.pr/` directory at the repository root. + +### Usage + +``` +.pr/ +├── design.md # Design decisions and architecture notes +├── analysis.md # Investigation or debugging notes +├── logs/ # Test output or CI logs for reviewer reference +└── notes.md # Any other PR-specific content +``` + +### How It Works + +1. **Notification**: When `.pr/` exists, a comment is posted to the PR conversation alerting reviewers +2. **Auto-cleanup**: When the PR is approved, the `.pr/` directory is automatically removed via `.github/workflows/pr-artifacts.yml` +3. **Fork PRs**: Auto-cleanup cannot push to forks, so manual removal is required before merging + +### Important Notes + +- Do NOT put anything in `.pr/` that needs to be preserved after merge +- The `.pr/` check passes (green ✅) during development — it only posts a notification, not a blocking error +- For fork PRs: You must manually remove `.pr/` before the PR can be merged + +### When to Use + +- Complex refactoring that benefits from written design rationale +- Debugging sessions where you want to document your investigation +- E2E test results or logs that demonstrate a cross-repo feature works +- Feature implementations that need temporary planning docs +- Any analysis that helps reviewers understand the PR but isn't needed long-term + ## Repository Structure Backend: - Located in the `openhands` directory From fb23418803e463a356778a172e2e8eadce70f1dd Mon Sep 17 00:00:00 2001 From: aivong-openhands Date: Wed, 18 Mar 2026 11:03:56 -0500 Subject: [PATCH 07/21] clarify docstring for provider token reference (#13386) Co-authored-by: openhands --- openhands/integrations/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands/integrations/provider.py b/openhands/integrations/provider.py index aa210eae18..ddec99d033 100644 --- a/openhands/integrations/provider.py +++ b/openhands/integrations/provider.py @@ -471,7 +471,7 @@ class ProviderHandler: def check_cmd_action_for_provider_token_ref( cls, event: Action ) -> list[ProviderType]: - """Detect if agent run action is using a provider token (e.g $GITHUB_TOKEN) + """Detect if agent run action is using a provider token (e.g github_token) Returns a list of providers which are called by the agent """ if not isinstance(event, CmdRunAction): From fb7333aa62aee39e86bd4c3a659a4f3be2e5074e Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Wed, 18 Mar 2026 17:10:07 +0100 Subject: [PATCH 08/21] fix: stop calling agent-server /generate_title (#13093) Co-authored-by: openhands --- .../set_title_callback_processor.py | 48 ++- .../test_set_title_callback_processor.py | 294 ++++++++++++++++++ 2 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 tests/unit/app_server/test_set_title_callback_processor.py diff --git a/openhands/app_server/event_callback/set_title_callback_processor.py b/openhands/app_server/event_callback/set_title_callback_processor.py index 071d28b5f9..37d70370b0 100644 --- a/openhands/app_server/event_callback/set_title_callback_processor.py +++ b/openhands/app_server/event_callback/set_title_callback_processor.py @@ -1,6 +1,9 @@ +import asyncio import logging from uuid import UUID +import httpx + from openhands.app_server.app_conversation.app_conversation_models import ( AppConversationInfo, ) @@ -22,6 +25,9 @@ from openhands.sdk import Event, MessageEvent _logger = logging.getLogger(__name__) +# Poll with ~3.75s total wait per message event before retrying later. +_TITLE_POLL_DELAYS_S = (0.25, 0.5, 1.0, 2.0) + class SetTitleCallbackProcessor(EventCallbackProcessor): """Callback processor which sets conversation titles.""" @@ -51,7 +57,6 @@ class SetTitleCallbackProcessor(EventCallbackProcessor): get_app_conversation_info_service(state) as app_conversation_info_service, get_httpx_client(state) as httpx_client, ): - # Generate a title for the conversation app_conversation = await app_conversation_service.get_app_conversation( conversation_id ) @@ -61,15 +66,38 @@ class SetTitleCallbackProcessor(EventCallbackProcessor): app_conversation_url = replace_localhost_hostname_for_docker( app_conversation_url ) - response = await httpx_client.post( - f'{app_conversation_url}/generate_title', - headers={ - 'X-Session-API-Key': app_conversation.session_api_key, - }, - content='{}', - ) - response.raise_for_status() - title = response.json()['title'] + + title = None + for delay_s in _TITLE_POLL_DELAYS_S: + try: + response = await httpx_client.get( + app_conversation_url, + headers={ + 'X-Session-API-Key': app_conversation.session_api_key, + }, + ) + response.raise_for_status() + except httpx.HTTPError as exc: + # Transient agent-server failures are acceptable; retry later. + _logger.debug( + 'Title poll failed for conversation %s: %s', + conversation_id, + exc, + ) + else: + title = response.json().get('title') + if title: + break + # Backoff applies to both missing-title responses and transient errors. + await asyncio.sleep(delay_s) + + if not title: + # Keep the callback active so later message events can retry. + _logger.info( + f'Conversation {conversation_id} title not available yet; ' + 'will retry on a future message event.' + ) + return None # Save the conversation info info = AppConversationInfo( diff --git a/tests/unit/app_server/test_set_title_callback_processor.py b/tests/unit/app_server/test_set_title_callback_processor.py new file mode 100644 index 0000000000..2c992be3f9 --- /dev/null +++ b/tests/unit/app_server/test_set_title_callback_processor.py @@ -0,0 +1,294 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from unittest.mock import AsyncMock, patch +from uuid import uuid4 + +import httpx +import pytest + +from openhands.app_server.app_conversation.app_conversation_models import ( + AppConversation, +) +from openhands.app_server.event_callback.event_callback_models import ( + EventCallback, + EventCallbackStatus, +) +from openhands.app_server.event_callback.set_title_callback_processor import ( + SetTitleCallbackProcessor, +) +from openhands.app_server.utils.docker_utils import ( + replace_localhost_hostname_for_docker, +) +from openhands.sdk import Message, MessageEvent, TextContent + + +class _FakeHttpxClient: + def __init__(self, titles: list[str | None]): + self._titles = titles + self.calls: list[tuple[str, dict[str, str] | None]] = [] + + async def get(self, url: str, headers: dict[str, str] | None = None): + self.calls.append((url, headers)) + idx = min(len(self.calls) - 1, len(self._titles) - 1) + request = httpx.Request('GET', url) + return httpx.Response(200, json={'title': self._titles[idx]}, request=request) + + +class _FailingHttpxClient: + def __init__(self, error: httpx.HTTPError): + self._error = error + self.calls: list[tuple[str, dict[str, str] | None]] = [] + + async def get(self, url: str, headers: dict[str, str] | None = None): + self.calls.append((url, headers)) + raise self._error + + +@asynccontextmanager +async def _ctx(obj): + yield obj + + +@pytest.mark.asyncio +async def test_set_title_callback_processor_fetches_title_from_conversation(): + conversation_id = uuid4() + session_api_key = 'test-session-key' + conversation_url = f'http://localhost:8000/api/conversations/{conversation_id.hex}' + + app_conversation = AppConversation( + id=conversation_id, + created_by_user_id='user', + sandbox_id='sandbox', + title=f'Conversation {conversation_id.hex[:5]}', + conversation_url=conversation_url, + session_api_key=session_api_key, + ) + + app_conversation_service = AsyncMock() + app_conversation_service.get_app_conversation.return_value = app_conversation + + app_conversation_info_service = AsyncMock() + event_callback_service = AsyncMock() + + httpx_client = _FakeHttpxClient(titles=[None, None, None, 'Generated Title']) + + def get_app_conversation_service(_state): + return _ctx(app_conversation_service) + + def get_app_conversation_info_service(_state): + return _ctx(app_conversation_info_service) + + def get_event_callback_service(_state): + return _ctx(event_callback_service) + + def get_httpx_client(_state): + return _ctx(httpx_client) + + callback = EventCallback( + conversation_id=conversation_id, processor=SetTitleCallbackProcessor() + ) + event = MessageEvent( + source='user', + llm_message=Message(role='user', content=[TextContent(text='hi')]), + ) + + processor = SetTitleCallbackProcessor() + + with ( + patch( + 'openhands.app_server.config.get_app_conversation_service', + get_app_conversation_service, + ), + patch( + 'openhands.app_server.config.get_app_conversation_info_service', + get_app_conversation_info_service, + ), + patch( + 'openhands.app_server.config.get_event_callback_service', + get_event_callback_service, + ), + patch('openhands.app_server.config.get_httpx_client', get_httpx_client), + patch( + 'openhands.app_server.event_callback.' + 'set_title_callback_processor.asyncio.sleep', + new=AsyncMock(), + ), + ): + result = await processor(conversation_id, callback, event) + + assert result is not None + + assert len(httpx_client.calls) == 4 + expected_url = replace_localhost_hostname_for_docker(conversation_url) + assert httpx_client.calls[0][0] == expected_url + assert httpx_client.calls[0][1] == {'X-Session-API-Key': session_api_key} + + app_conversation_info_service.save_app_conversation_info.assert_called_once() + saved_info = app_conversation_info_service.save_app_conversation_info.call_args[0][ + 0 + ] + assert saved_info.title == 'Generated Title' + + assert callback.status == EventCallbackStatus.DISABLED + event_callback_service.save_event_callback.assert_called_once() + + +@pytest.mark.asyncio +async def test_set_title_callback_processor_no_title_yet_returns_none(): + conversation_id = uuid4() + session_api_key = 'test-session-key' + conversation_url = f'http://localhost:8000/api/conversations/{conversation_id.hex}' + + app_conversation = AppConversation( + id=conversation_id, + created_by_user_id='user', + sandbox_id='sandbox', + title=f'Conversation {conversation_id.hex[:5]}', + conversation_url=conversation_url, + session_api_key=session_api_key, + ) + + app_conversation_service = AsyncMock() + app_conversation_service.get_app_conversation.return_value = app_conversation + + app_conversation_info_service = AsyncMock() + event_callback_service = AsyncMock() + + httpx_client = _FakeHttpxClient(titles=[None]) + + def get_app_conversation_service(_state): + return _ctx(app_conversation_service) + + def get_app_conversation_info_service(_state): + return _ctx(app_conversation_info_service) + + def get_event_callback_service(_state): + return _ctx(event_callback_service) + + def get_httpx_client(_state): + return _ctx(httpx_client) + + callback = EventCallback( + conversation_id=conversation_id, processor=SetTitleCallbackProcessor() + ) + event = MessageEvent( + source='user', + llm_message=Message(role='user', content=[TextContent(text='hi')]), + ) + + processor = SetTitleCallbackProcessor() + + with ( + patch( + 'openhands.app_server.config.get_app_conversation_service', + get_app_conversation_service, + ), + patch( + 'openhands.app_server.config.get_app_conversation_info_service', + get_app_conversation_info_service, + ), + patch( + 'openhands.app_server.config.get_event_callback_service', + get_event_callback_service, + ), + patch('openhands.app_server.config.get_httpx_client', get_httpx_client), + patch( + 'openhands.app_server.event_callback.' + 'set_title_callback_processor.asyncio.sleep', + new=AsyncMock(), + ), + ): + result = await processor(conversation_id, callback, event) + + assert result is None + + app_conversation_info_service.save_app_conversation_info.assert_not_called() + event_callback_service.save_event_callback.assert_not_called() + assert callback.status == EventCallbackStatus.ACTIVE + + +@pytest.mark.asyncio +async def test_set_title_callback_processor_request_errors_return_none(): + conversation_id = uuid4() + session_api_key = 'test-session-key' + conversation_url = f'http://localhost:8000/api/conversations/{conversation_id.hex}' + + app_conversation = AppConversation( + id=conversation_id, + created_by_user_id='user', + sandbox_id='sandbox', + title=f'Conversation {conversation_id.hex[:5]}', + conversation_url=conversation_url, + session_api_key=session_api_key, + ) + + app_conversation_service = AsyncMock() + app_conversation_service.get_app_conversation.return_value = app_conversation + + app_conversation_info_service = AsyncMock() + event_callback_service = AsyncMock() + + httpx_client = _FailingHttpxClient( + httpx.RequestError( + 'boom', + request=httpx.Request( + 'GET', replace_localhost_hostname_for_docker(conversation_url) + ), + ) + ) + + def get_app_conversation_service(_state): + return _ctx(app_conversation_service) + + def get_app_conversation_info_service(_state): + return _ctx(app_conversation_info_service) + + def get_event_callback_service(_state): + return _ctx(event_callback_service) + + def get_httpx_client(_state): + return _ctx(httpx_client) + + callback = EventCallback( + conversation_id=conversation_id, processor=SetTitleCallbackProcessor() + ) + event = MessageEvent( + source='user', + llm_message=Message(role='user', content=[TextContent(text='hi')]), + ) + + processor = SetTitleCallbackProcessor() + + with ( + patch( + 'openhands.app_server.config.get_app_conversation_service', + get_app_conversation_service, + ), + patch( + 'openhands.app_server.config.get_app_conversation_info_service', + get_app_conversation_info_service, + ), + patch( + 'openhands.app_server.config.get_event_callback_service', + get_event_callback_service, + ), + patch('openhands.app_server.config.get_httpx_client', get_httpx_client), + patch( + 'openhands.app_server.event_callback.' + 'set_title_callback_processor.asyncio.sleep', + new=AsyncMock(), + ), + patch( + 'openhands.app_server.event_callback.' + 'set_title_callback_processor._logger.debug' + ) as logger_debug, + ): + result = await processor(conversation_id, callback, event) + + assert result is None + assert len(httpx_client.calls) == 4 + assert logger_debug.call_count == 4 + app_conversation_info_service.save_app_conversation_info.assert_not_called() + event_callback_service.save_event_callback.assert_not_called() + assert callback.status == EventCallbackStatus.ACTIVE From eb9a822d4c5ad3283d12cc6c1dc5c72158862004 Mon Sep 17 00:00:00 2001 From: Jamie Chicago <87397251+jamiechicago312@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:10:22 -0500 Subject: [PATCH 09/21] Update CONTRIBUTING.md (#13463) --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d30510981..13e601937b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,6 +125,17 @@ For example, a PR title could be: - If your changes are user-facing (e.g. a new feature in the UI, a change in behavior, or a bugfix), please include a short message that we can add to our changelog +## Becoming a Maintainer + +For contributors who have made significant and sustained contributions to the project, there is a possibility of joining the maintainer team. +The process for this is as follows: + +1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated. +2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days. +3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote. + +Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md). + ## Need Help? - **Slack**: [Join our community](https://openhands.dev/joinslack) From c62b47dcb13daa567860ae624a1fb32787b2dd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=81=9A=E4=BA=86=E7=9D=A1=E5=A4=A7=E8=A7=89?= <64798754+stakeswky@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:36:52 +0800 Subject: [PATCH 10/21] fix: handle empty body in GitHub issue resolver (#13039) Co-authored-by: User --- openhands/resolver/interfaces/issue.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openhands/resolver/interfaces/issue.py b/openhands/resolver/interfaces/issue.py index d57ce22412..a456be5c70 100644 --- a/openhands/resolver/interfaces/issue.py +++ b/openhands/resolver/interfaces/issue.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from typing import Any -from pydantic import BaseModel +from pydantic import BaseModel, field_validator class ReviewThread(BaseModel): @@ -21,7 +21,13 @@ class Issue(BaseModel): repo: str number: int title: str - body: str + body: str = '' + + @field_validator('body', mode='before') + @classmethod + def body_must_not_be_none(cls, v: str | None) -> str: + return v if v is not None else '' + thread_comments: list[str] | None = None # Added field for issue thread comments closing_issues: list[str] | None = None review_comments: list[str] | None = None From 48cd85e47e7f87c5cc64ceafa28b4c412333e96f Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Wed, 18 Mar 2026 12:04:36 -0500 Subject: [PATCH 11/21] fix(security): add sleep to container wait loop (#12869) Co-authored-by: Claude Opus 4.6 --- openhands/security/invariant/analyzer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openhands/security/invariant/analyzer.py b/openhands/security/invariant/analyzer.py index bba78e4ace..a9548857c6 100644 --- a/openhands/security/invariant/analyzer.py +++ b/openhands/security/invariant/analyzer.py @@ -6,6 +6,7 @@ # Unless you are working on deprecation, please avoid extending this legacy file and consult the V1 codepaths above. # Tag: Legacy-V0 import re +import time import uuid from typing import Any @@ -71,15 +72,16 @@ class InvariantAnalyzer(SecurityAnalyzer): else: self.container = running_containers[0] - elapsed = 0 + start_time = time.time() while self.container.status != 'running': self.container = self.docker_client.containers.get(self.container_name) - elapsed += 1 + elapsed = time.time() - start_time logger.debug( - f'waiting for container to start: {elapsed}, container status: {self.container.status}' + f'waiting for container to start: {elapsed:.1f}s, container status: {self.container.status}' ) if elapsed > self.timeout: break + time.sleep(0.5) self.api_port = int( self.container.attrs['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'] From 8e0386c4163e9e2d16a68a974dbd2150a6b1af12 Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Wed, 18 Mar 2026 18:17:43 +0100 Subject: [PATCH 12/21] feat: add Catalan translation (#13299) --- frontend/src/i18n/index.ts | 1 + frontend/src/i18n/translation.json | 3676 +++++++++++++++++++--------- 2 files changed, 2501 insertions(+), 1176 deletions(-) diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index f7a57b52a5..3234f41c11 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -16,6 +16,7 @@ export const AvailableLanguages = [ { label: "Italiano", value: "it" }, { label: "Português", value: "pt" }, { label: "Español", value: "es" }, + { label: "Català", value: "ca" }, { label: "Türkçe", value: "tr" }, { label: "Українська", value: "uk" }, ]; diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 93ec856439..f43c33b0d2 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -13,7 +13,8 @@ "fr": "La maintenance programmée commencera à {{time}}", "tr": "Planlı bakım {{time}} tarihinde başlayacak", "de": "Die geplante Wartung beginnt um {{time}}", - "uk": "Планове технічне обслуговування розпочнеться о {{time}}" + "uk": "Планове технічне обслуговування розпочнеться о {{time}}", + "ca": "El manteniment programat començarà a les {{time}}" }, "ALERT$FAULTY_MODELS_MESSAGE": { "en": "The following models are currently reporting errors:", @@ -29,7 +30,8 @@ "fr": "Les modèles suivants signalent actuellement des erreurs :", "tr": "Aşağıdaki modeller şu anda hata bildiriyor:", "de": "Die folgenden Modelle melden derzeit Fehler:", - "uk": "Наступні моделі наразі повідомляють про помилки:" + "uk": "Наступні моделі наразі повідомляють про помилки:", + "ca": "Els models següents estan informant d'errors actualment:" }, "AZURE_DEVOPS$CONNECT_ACCOUNT": { "en": "Connect Azure DevOps Account", @@ -45,7 +47,8 @@ "fr": "Connecter le compte Azure DevOps", "tr": "Azure DevOps hesabını bağla", "de": "Azure DevOps-Konto verbinden", - "uk": "Підключити обліковий запис Azure DevOps" + "uk": "Підключити обліковий запис Azure DevOps", + "ca": "Connecta el compte d'Azure DevOps" }, "GIT$AZURE_DEVOPS_TOKEN": { "en": "Azure DevOps Personal Access Token", @@ -61,7 +64,8 @@ "fr": "Jeton d'accès personnel Azure DevOps", "tr": "Azure DevOps kişisel erişim belirteci", "de": "Azure DevOps persönliches Zugriffstoken", - "uk": "Персональний токен доступу Azure DevOps" + "uk": "Персональний токен доступу Azure DevOps", + "ca": "Token d'accés personal d'Azure DevOps" }, "GIT$AZURE_DEVOPS_HOST": { "en": "Azure DevOps Organization", @@ -77,7 +81,8 @@ "fr": "Organisation Azure DevOps", "tr": "Azure DevOps kuruluş", "de": "Azure DevOps Organisation", - "uk": "Організація Azure DevOps" + "uk": "Організація Azure DevOps", + "ca": "Organització d'Azure DevOps" }, "GIT$AZURE_DEVOPS_HOST_PLACEHOLDER": { "en": "organization", @@ -93,7 +98,8 @@ "fr": "organisation", "tr": "kuruluş/proje", "de": "organisation/projekt", - "uk": "організація/проект" + "uk": "організація/проект", + "ca": "organització" }, "GIT$AZURE_DEVOPS_TOKEN_HELP": { "en": "How to create an Azure DevOps token", @@ -109,7 +115,8 @@ "fr": "Comment créer un jeton Azure DevOps", "tr": "Azure DevOps belirteci nasıl oluşturulur", "de": "Wie man ein Azure DevOps-Token erstellt", - "uk": "Як створити токен Azure DevOps" + "uk": "Як створити токен Azure DevOps", + "ca": "Com crear un token d'Azure DevOps" }, "MICROAGENT$NO_REPOSITORY_FOUND": { "en": "No repository found to launch microagent", @@ -125,7 +132,8 @@ "fr": "Aucun dépôt trouvé pour lancer le micro-agent", "tr": "Mikro ajanı başlatmak için depo bulunamadı", "de": "Kein Repository gefunden, um Microagent zu starten", - "uk": "Не знайдено репозиторій для запуску мікроагента" + "uk": "Не знайдено репозиторій для запуску мікроагента", + "ca": "No s'ha trobat cap repositori per llançar el microagent" }, "MICROAGENT$ADD_TO_MICROAGENT": { "en": "Add to Microagent", @@ -141,7 +149,8 @@ "fr": "Ajouter au micro-agent", "tr": "Mikro ajana ekle", "de": "Zum Microagent hinzufügen", - "uk": "Додати до мікроагента" + "uk": "Додати до мікроагента", + "ca": "Afegeix al Microagent" }, "MICROAGENT$WHAT_TO_ADD": { "en": "What would you like to add to the Microagent?", @@ -157,7 +166,8 @@ "fr": "Que souhaitez-vous ajouter au micro-agent ?", "tr": "Mikro ajana ne eklemek istersiniz?", "de": "Was möchten Sie zum Microagent hinzufügen?", - "uk": "Що ви хочете додати до мікроагента?" + "uk": "Що ви хочете додати до мікроагента?", + "ca": "Què vols afegir al Microagent?" }, "MICROAGENT$WHERE_TO_PUT": { "en": "Where should we put it?", @@ -173,7 +183,8 @@ "fr": "Où devons-nous le mettre ?", "tr": "Nereye koyalım?", "de": "Wo sollen wir es platzieren?", - "uk": "Куди ми повинні його помістити?" + "uk": "Куди ми повинні його помістити?", + "ca": "On ho hem de posar?" }, "MICROAGENT$ADD_TRIGGER": { "en": "Add a trigger for the microagent", @@ -189,7 +200,8 @@ "fr": "Ajouter un déclencheur pour le micro-agent", "tr": "Mikro ajan için bir tetikleyici ekleyin", "de": "Fügen Sie einen Auslöser für den Microagent hinzu", - "uk": "Додати тригер для мікроагента" + "uk": "Додати тригер для мікроагента", + "ca": "Afegeix un disparador per al microagent" }, "MICROAGENT$WHAT_TO_REMEMBER": { "en": "What would you like your microagent to remember?", @@ -205,7 +217,8 @@ "fr": "Que souhaitez-vous que votre micro-agent se souvienne ?", "tr": "Mikro ajanınızın neyi hatırlamasını istersiniz?", "de": "Was soll sich Ihr Microagent merken?", - "uk": "Що ви хочете, щоб ваш мікроагент запам'ятав?" + "uk": "Що ви хочете, щоб ваш мікроагент запам'ятав?", + "ca": "Què vols que el teu microagent recordi?" }, "MICROAGENT$ADD_TRIGGERS": { "en": "Add triggers for the microagent", @@ -221,7 +234,8 @@ "fr": "Ajouter des déclencheurs pour le micro-agent", "tr": "Mikro ajan için tetikleyiciler ekleyin", "de": "Auslöser für den Microagent hinzufügen", - "uk": "Додати тригери для мікроагента" + "uk": "Додати тригери для мікроагента", + "ca": "Afegeix disparadors per al microagent" }, "MICROAGENT$WAIT_FOR_RUNTIME": { "en": "Please wait for the runtime to be active.", @@ -237,7 +251,8 @@ "fr": "Veuillez attendre que le runtime soit actif.", "tr": "Lütfen çalışma zamanının aktif olmasını bekleyin.", "de": "Bitte warten Sie, bis die Laufzeitumgebung aktiv ist.", - "uk": "Будь ласка, зачекайте, поки середовище виконання стане активним." + "uk": "Будь ласка, зачекайте, поки середовище виконання стане активним.", + "ca": "Espereu que l'entorn d'execució estigui actiu." }, "FORGEJO$TOKEN_LABEL": { "en": "Forgejo Personal Access Token", @@ -253,7 +268,8 @@ "fr": "Jeton d'accès personnel Forgejo", "tr": "Forgejo kişisel erişim belirteci", "de": "Forgejo persönliches Zugriffstoken", - "uk": "Персональний токен доступу Forgejo" + "uk": "Персональний токен доступу Forgejo", + "ca": "Token d'accés personal de Forgejo" }, "FORGEJO$HOST_LABEL": { "en": "Forgejo Host (domain)", @@ -269,7 +285,8 @@ "fr": "Hôte Forgejo (domaine)", "tr": "Forgejo ana makinesi (alan adı)", "de": "Forgejo Host (Domain)", - "uk": "Хост Forgejo (домен)" + "uk": "Хост Forgejo (домен)", + "ca": "Servidor de Forgejo (domini)" }, "MICROAGENT$ADDING_CONTEXT": { "en": "OpenHands is adding this new context to your respository. We'll let you know when the pull request is ready.", @@ -285,7 +302,8 @@ "fr": "OpenHands ajoute ce nouveau contexte à votre dépôt. Nous vous informerons lorsque la pull request sera prête.", "tr": "OpenHands bu yeni bağlamı deponuza ekliyor. Çekme isteği hazır olduğunda size haber vereceğiz.", "de": "OpenHands fügt diesen neuen Kontext zu Ihrem Repository hinzu. Wir informieren Sie, wenn der Pull Request bereit ist.", - "uk": "OpenHands додає цей новий контекст до вашого репозиторію. Ми повідомимо вас, коли запит на витягування буде готовий." + "uk": "OpenHands додає цей новий контекст до вашого репозиторію. Ми повідомимо вас, коли запит на витягування буде готовий.", + "ca": "OpenHands està afegint aquest nou context al vostre repositori. Us avisarem quan la sol·licitud de canvis estigui preparada." }, "MICROAGENT$VIEW_CONVERSATION": { "en": "View Conversation", @@ -301,7 +319,8 @@ "fr": "Voir la conversation", "tr": "Konuşmayı Görüntüle", "de": "Konversation anzeigen", - "uk": "Переглянути розмову" + "uk": "Переглянути розмову", + "ca": "Veure la conversa" }, "MICROAGENT$SUCCESS_PR_READY": { "en": "Success! Your microagent pull request is ready.", @@ -317,7 +336,8 @@ "fr": "Succès ! Votre pull request de micro-agent est prête.", "tr": "Başarılı! Mikro ajan çekme isteğiniz hazır.", "de": "Erfolg! Ihr Microagent Pull Request ist bereit.", - "uk": "Успіх! Ваш запит на витягування мікроагента готовий." + "uk": "Успіх! Ваш запит на витягування мікроагента готовий.", + "ca": "Fet! La sol·licitud de canvis del microagent està preparada." }, "MICROAGENT$STATUS_CREATING": { "en": "Modifying microagent...", @@ -333,7 +353,8 @@ "fr": "Modification du micro-agent en cours...", "tr": "Mikro ajan değiştiriliyor...", "de": "Microagent wird geändert...", - "uk": "Зміна мікроагента..." + "uk": "Зміна мікроагента...", + "ca": "Modificant el microagent..." }, "MICROAGENT$STATUS_OPENING_PR": { "en": "Opening PR", @@ -349,7 +370,8 @@ "fr": "Ouverture de la PR", "tr": "PR açılıyor", "de": "PR wird geöffnet", - "uk": "Відкриття PR" + "uk": "Відкриття PR", + "ca": "Obrint la PR" }, "MICROAGENT$STATUS_COMPLETED": { "en": "View microagent update", @@ -365,7 +387,8 @@ "fr": "Voir la mise à jour du micro-agent", "tr": "Mikro ajan güncellemesini görüntüle", "de": "Microagent-Update anzeigen", - "uk": "Переглянути оновлення мікроагента" + "uk": "Переглянути оновлення мікроагента", + "ca": "Veure l'actualització del microagent" }, "MICROAGENT$STATUS_ERROR": { "en": "Microagent encountered an error", @@ -381,7 +404,8 @@ "fr": "Le micro-agent a rencontré une erreur", "tr": "Mikro ajan bir hatayla karşılaştı", "de": "Microagent ist auf einen Fehler gestoßen", - "uk": "Мікроагент зіткнувся з помилкою" + "uk": "Мікроагент зіткнувся з помилкою", + "ca": "El microagent ha trobat un error" }, "MICROAGENT$VIEW_YOUR_PR": { "en": "View your PR", @@ -397,7 +421,8 @@ "fr": "Voir votre PR", "tr": "PR'ınızı görüntüleyin", "de": "Ihre PR anzeigen", - "uk": "Переглянути ваш PR" + "uk": "Переглянути ваш PR", + "ca": "Veure la teva PR" }, "MICROAGENT$DESCRIBE_WHAT_TO_ADD": { "en": "Describe what you want to add to the Microagent...", @@ -413,7 +438,8 @@ "fr": "Décrivez ce que vous souhaitez ajouter au micro-agent...", "tr": "Mikro ajana eklemek istediğinizi açıklayın...", "de": "Beschreiben Sie, was Sie zum Microagent hinzufügen möchten...", - "uk": "Опишіть, що ви хочете додати до мікроагента..." + "uk": "Опишіть, що ви хочете додати до мікроагента...", + "ca": "Descriu el que vols afegir al Microagent..." }, "MICROAGENT$SELECT_FILE_OR_CUSTOM": { "en": "Select a microagent file or enter a custom value", @@ -429,7 +455,8 @@ "fr": "Sélectionnez un fichier micro-agent ou entrez une valeur personnalisée", "tr": "Bir mikro ajan dosyası seçin veya özel bir değer girin", "de": "Wählen Sie eine Microagent-Datei aus oder geben Sie einen benutzerdefinierten Wert ein", - "uk": "Виберіть файл мікроагента або введіть власне значення" + "uk": "Виберіть файл мікроагента або введіть власне значення", + "ca": "Selecciona un fitxer de microagent o introdueix un valor personalitzat" }, "MICROAGENT$TYPE_TRIGGER_SPACE": { "en": "Type a trigger and press Space to add it", @@ -445,7 +472,8 @@ "fr": "Tapez un déclencheur et appuyez sur Espace pour l'ajouter", "tr": "Bir tetikleyici yazın ve eklemek için Boşluk tuşuna basın", "de": "Geben Sie einen Auslöser ein und drücken Sie die Leertaste, um ihn hinzuzufügen", - "uk": "Введіть тригер і натисніть пробіл, щоб додати його" + "uk": "Введіть тригер і натисніть пробіл, щоб додати його", + "ca": "Escriu un disparador i prem Espai per afegir-lo" }, "MICROAGENT$LOADING_PROMPT": { "en": "Loading prompt...", @@ -461,7 +489,8 @@ "fr": "Chargement du prompt...", "tr": "İstem yükleniyor...", "de": "Prompt wird geladen...", - "uk": "Завантаження підказки..." + "uk": "Завантаження підказки...", + "ca": "Carregant el missatge..." }, "MICROAGENT$CANCEL": { "en": "Cancel", @@ -477,7 +506,8 @@ "fr": "Annuler", "tr": "İptal", "de": "Abbrechen", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "MICROAGENT$LAUNCH": { "en": "Launch", @@ -493,7 +523,8 @@ "fr": "Lancer", "tr": "Başlat", "de": "Starten", - "uk": "Запустити" + "uk": "Запустити", + "ca": "Llança" }, "STATUS$WEBSOCKET_CLOSED": { "en": "The WebSocket connection was closed.", @@ -509,7 +540,8 @@ "fr": "La connexion WebSocket a été fermée.", "tr": "WebSocket bağlantısı kapatıldı.", "de": "Die WebSocket-Verbindung wurde geschlossen.", - "uk": "З'єднання WebSocket було закрито." + "uk": "З'єднання WebSocket було закрито.", + "ca": "La connexió WebSocket s'ha tancat." }, "HOME$LAUNCH_FROM_SCRATCH": { "en": "Launch from Scratch", @@ -525,7 +557,8 @@ "fr": "Démarrer de zéro", "tr": "Sıfırdan başla", "de": "Von Grund auf starten", - "uk": "Почати з нуля" + "uk": "Почати з нуля", + "ca": "Comença des de zero" }, "HOME$READ_THIS": { "en": "Read this", @@ -541,7 +574,8 @@ "fr": "Lire ceci", "tr": "Bunu oku", "de": "Lies dies", - "uk": "Прочитайте це" + "uk": "Прочитайте це", + "ca": "Llegeix això" }, "AUTH$LOGGING_BACK_IN": { "en": "Logging back into OpenHands...", @@ -557,7 +591,8 @@ "fr": "Reconnexion à OpenHands...", "tr": "OpenHands'e yeniden giriş yapılıyor...", "de": "Erneute Anmeldung bei OpenHands...", - "uk": "Повторний вхід до OpenHands..." + "uk": "Повторний вхід до OpenHands...", + "ca": "Tornant a iniciar sessió a OpenHands..." }, "SECURITY$LOW_RISK": { "en": "Risk: Low", @@ -573,7 +608,8 @@ "fr": "Risque : Faible", "tr": "Risk: Düşük", "de": "Risiko: Gering", - "uk": "Ризик: Низький" + "uk": "Ризик: Низький", + "ca": "Risc: Baix" }, "SECURITY$MEDIUM_RISK": { "en": "Risk: Medium", @@ -589,7 +625,8 @@ "fr": "Risque : Moyen", "tr": "Risk: Orta", "de": "Risiko: Mittel", - "uk": "Ризик: Середній" + "uk": "Ризик: Середній", + "ca": "Risc: Mitjà" }, "SECURITY$HIGH_RISK": { "en": "Risk: High", @@ -605,7 +642,8 @@ "fr": "Risque : Élevé", "tr": "Risk: Yüksek", "de": "Risiko: Hoch", - "uk": "Ризик: Високий" + "uk": "Ризик: Високий", + "ca": "Risc: Alt" }, "SECURITY$UNKNOWN_RISK": { "en": "Risk: Unknown", @@ -621,7 +659,8 @@ "fr": "Risque : Inconnu", "tr": "Risk: Bilinmeyen", "de": "Risiko: Unbekannt", - "uk": "Ризик: Невідомий" + "uk": "Ризик: Невідомий", + "ca": "Risc: Desconegut" }, "FINISH$TASK_COMPLETED_SUCCESSFULLY": { "en": "I believe that the task was **completed successfully**.", @@ -637,7 +676,8 @@ "fr": "Je pense que la tâche a été **accomplie avec succès**.", "tr": "Görevin **başarıyla tamamlandığına** inanıyorum.", "de": "Ich glaube, dass die Aufgabe **erfolgreich abgeschlossen** wurde.", - "uk": "Я вважаю, що завдання було **успішно виконано**." + "uk": "Я вважаю, що завдання було **успішно виконано**.", + "ca": "Crec que la tasca s'ha **completat correctament**." }, "FINISH$TASK_NOT_COMPLETED": { "en": "I believe that the task was **not completed**.", @@ -653,7 +693,8 @@ "fr": "Je pense que la tâche **n'a pas été accomplie**.", "tr": "Görevin **tamamlanmadığına** inanıyorum.", "de": "Ich glaube, dass die Aufgabe **nicht abgeschlossen** wurde.", - "uk": "Я вважаю, що завдання **не було виконано**." + "uk": "Я вважаю, що завдання **не було виконано**.", + "ca": "Crec que la tasca **no s'ha completat**." }, "FINISH$TASK_COMPLETED_PARTIALLY": { "en": "I believe that the task was **completed partially**.", @@ -669,7 +710,8 @@ "fr": "Je pense que la tâche a été **partiellement accomplie**.", "tr": "Görevin **kısmen tamamlandığına** inanıyorum.", "de": "Ich glaube, dass die Aufgabe **teilweise abgeschlossen** wurde.", - "uk": "Я вважаю, що завдання було **частково виконано**." + "uk": "Я вважаю, що завдання було **частково виконано**.", + "ca": "Crec que la tasca s'ha **completat parcialment**." }, "EVENT$UNKNOWN_EVENT": { "en": "Unknown event", @@ -685,7 +727,8 @@ "fr": "Événement inconnu", "tr": "Bilinmeyen olay", "de": "Unbekanntes Ereignis", - "uk": "Невідома подія" + "uk": "Невідома подія", + "ca": "Esdeveniment desconegut" }, "OBSERVATION$COMMAND_NO_OUTPUT": { "en": "[Command finished execution with no output]", @@ -701,7 +744,8 @@ "fr": "[La commande s'est terminée sans sortie]", "tr": "[Komut çıktı olmadan yürütmeyi tamamladı]", "de": "[Befehl wurde ohne Ausgabe ausgeführt]", - "uk": "[Команда завершила виконання без виводу]" + "uk": "[Команда завершила виконання без виводу]", + "ca": "[La comanda ha finalitzat l'execució sense cap sortida]" }, "OBSERVATION$MCP_NO_OUTPUT": { "en": "[MCP Tool finished execution with no output]", @@ -717,7 +761,8 @@ "fr": "[L'outil MCP s'est terminé sans sortie]", "tr": "[MCP Aracı çıktı olmadan yürütmeyi tamamladı]", "de": "[MCP-Tool wurde ohne Ausgabe ausgeführt]", - "uk": "[Інструмент MCP завершив виконання без виводу]" + "uk": "[Інструмент MCP завершив виконання без виводу]", + "ca": "[L'eina MCP ha finalitzat l'execució sense cap sortida]" }, "OBSERVATION$TASK_TRACKING_NO_OUTPUT": { "en": "[Task tracking completed with no output]", @@ -733,7 +778,8 @@ "fr": "[Suivi des tâches terminé sans sortie]", "tr": "[Görev takibi çıktı olmadan tamamlandı]", "de": "[Aufgabenverfolgung ohne Ausgabe abgeschlossen]", - "uk": "[Відстеження завдань завершено без виводу]" + "uk": "[Відстеження завдань завершено без виводу]", + "ca": "[El seguiment de tasques ha finalitzat sense cap sortida]" }, "MCP_OBSERVATION$ARGUMENTS": { "en": "Arguments", @@ -749,7 +795,8 @@ "fr": "Arguments", "tr": "Argümanlar", "de": "Argumente", - "uk": "Аргументи" + "uk": "Аргументи", + "ca": "Arguments" }, "MCP_OBSERVATION$OUTPUT": { "en": "Output", @@ -765,7 +812,8 @@ "fr": "Sortie", "tr": "Çıktı", "de": "Ausgabe", - "uk": "Вивід" + "uk": "Вивід", + "ca": "Sortida" }, "TASK_TRACKING_OBSERVATION$TASK_LIST": { "en": "Task List", @@ -781,7 +829,8 @@ "fr": "Liste des tâches", "tr": "Görev listesi", "de": "Aufgabenliste", - "uk": "Список завдань" + "uk": "Список завдань", + "ca": "Llista de tasques" }, "TASK_TRACKING_OBSERVATION$OUTPUT": { "en": "Output", @@ -797,7 +846,8 @@ "fr": "Sortie", "tr": "Çıktı", "de": "Ausgabe", - "uk": "Вивід" + "uk": "Вивід", + "ca": "Sortida" }, "TASK_TRACKING_OBSERVATION$TASK_ID": { "en": "ID", @@ -813,7 +863,8 @@ "fr": "ID", "tr": "ID", "de": "ID", - "uk": "ID" + "uk": "ID", + "ca": "ID" }, "TASK_TRACKING_OBSERVATION$TASK_NOTES": { "en": "Notes", @@ -829,7 +880,8 @@ "fr": "Notes", "tr": "Notlar", "de": "Notizen", - "uk": "Примітки" + "uk": "Примітки", + "ca": "Notes" }, "TASK_TRACKING_OBSERVATION$RESULT": { "en": "Result", @@ -845,7 +897,8 @@ "fr": "Résultat", "tr": "Sonuç", "de": "Ergebnis", - "uk": "Результат" + "uk": "Результат", + "ca": "Resultat" }, "OBSERVATION$ERROR_PREFIX": { "en": "error:", @@ -861,7 +914,8 @@ "fr": "erreur:", "tr": "hata:", "de": "Fehler:", - "uk": "помилка:" + "uk": "помилка:", + "ca": "error:" }, "TASK$ADDRESSING_TASK": { "en": "Addressing task...", @@ -877,7 +931,8 @@ "fr": "Traitement de la tâche...", "tr": "Görev ele alınıyor...", "de": "Aufgabe wird bearbeitet...", - "uk": "Вирішення завдання..." + "uk": "Вирішення завдання...", + "ca": "Tractant la tasca..." }, "SECRETS$SECRET_VALUE_REQUIRED": { "en": "Secret value is required", @@ -893,7 +948,8 @@ "fr": "La valeur du secret est requise", "tr": "Gizli değer gereklidir", "de": "Geheimer Wert ist erforderlich", - "uk": "Значення секрету є обов'язковим" + "uk": "Значення секрету є обов'язковим", + "ca": "El valor del secret és obligatori" }, "SECRETS$ADD_SECRET": { "en": "Add secret", @@ -909,7 +965,8 @@ "fr": "Ajouter un secret", "tr": "Gizli ekle", "de": "Geheimnis hinzufügen", - "uk": "Додати секрет" + "uk": "Додати секрет", + "ca": "Afegeix un secret" }, "SECRETS$EDIT_SECRET": { "en": "Edit secret", @@ -925,7 +982,8 @@ "fr": "Modifier le secret", "tr": "Gizliyi düzenle", "de": "Geheimnis bearbeiten", - "uk": "Редагувати секрет" + "uk": "Редагувати секрет", + "ca": "Edita el secret" }, "SECRETS$ADD_NEW_SECRET": { "en": "Add a new secret", @@ -941,7 +999,8 @@ "fr": "Ajouter un nouveau secret", "tr": "Yeni bir gizli ekle", "de": "Neues Geheimnis hinzufügen", - "uk": "Додати новий секрет" + "uk": "Додати новий секрет", + "ca": "Afegeix un nou secret" }, "SECRETS$CONFIRM_DELETE_KEY": { "en": "Are you sure you want to delete this key?", @@ -957,7 +1016,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer cette clé ?", "tr": "Bu anahtarı silmek istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie diesen Schlüssel löschen möchten?", - "uk": "Ви впевнені, що хочете видалити цей ключ?" + "uk": "Ви впевнені, що хочете видалити цей ключ?", + "ca": "Esteu segur que voleu eliminar aquesta clau?" }, "SETTINGS$MCP_TITLE": { "en": "Model Context Protocol (MCP)", @@ -973,7 +1033,8 @@ "fr": "Protocole de Contexte de Modèle (MCP)", "tr": "Model Bağlam Protokolü (MCP)", "de": "Modellkontextprotokoll (MCP)", - "uk": "Протокол контексту моделі (MCP)" + "uk": "Протокол контексту моделі (MCP)", + "ca": "Protocol de Context de Model (MCP)" }, "SETTINGS$MCP_DESCRIPTION": { "en": "Configure MCP servers for enhanced model capabilities", @@ -989,7 +1050,8 @@ "fr": "Configurez les serveurs MCP pour des capacités de modèle améliorées", "tr": "Gelişmiş model yetenekleri için MCP sunucularını yapılandırın", "de": "Konfigurieren Sie MCP-Server für erweiterte Modellfunktionen", - "uk": "Налаштуйте сервери MCP для розширених можливостей моделі" + "uk": "Налаштуйте сервери MCP для розширених можливостей моделі", + "ca": "Configura els servidors MCP per a capacitats de model millorades" }, "SETTINGS$NAV_MCP": { "en": "MCP", @@ -1005,7 +1067,8 @@ "fr": "MCP", "tr": "MCP", "de": "MCP", - "uk": "MCP" + "uk": "MCP", + "ca": "MCP" }, "SETTINGS$MCP_CONFIGURATION": { "en": "MCP Configuration", @@ -1021,7 +1084,8 @@ "fr": "Configuration MCP", "tr": "MCP Yapılandırması", "de": "MCP-Konfiguration", - "uk": "Налаштування MCP" + "uk": "Налаштування MCP", + "ca": "Configuració MCP" }, "SETTINGS$MCP_EDIT_CONFIGURATION": { "en": "Edit Configuration", @@ -1037,7 +1101,8 @@ "fr": "Modifier la configuration", "tr": "Yapılandırmayı Düzenle", "de": "Konfiguration bearbeiten", - "uk": "Редагувати налаштування" + "uk": "Редагувати налаштування", + "ca": "Edita la configuració" }, "SETTINGS$MCP_PREVIEW_CHANGES": { "en": "Preview Changes", @@ -1053,7 +1118,8 @@ "fr": "Aperçu des modifications", "tr": "Değişiklikleri Önizle", "de": "Änderungen anzeigen", - "uk": "Переглянути зміни" + "uk": "Переглянути зміни", + "ca": "Previsualitza els canvis" }, "SETTINGS$MCP_CONFIG_DESCRIPTION": { "en": "Edit the JSON configuration for MCP servers below. The configuration must include both sse_servers and stdio_servers arrays. For full configuration details and integration examples, see the documentation.", @@ -1069,7 +1135,8 @@ "fr": "Modifiez la configuration JSON pour les serveurs MCP ci-dessous. La configuration doit inclure à la fois les tableaux sse_servers et stdio_servers. Pour plus de détails sur la configuration et des exemples d'intégration, voir la documentation.", "tr": "Aşağıdaki MCP sunucuları için JSON yapılandırmasını düzenleyin. Yapılandırma hem sse_servers hem de stdio_servers dizilerini içermelidir. Tam yapılandırma ayrıntıları ve entegrasyon örnekleri için belgeler'e bakın.", "de": "Bearbeiten Sie die JSON-Konfiguration für MCP-Server unten. Die Konfiguration muss sowohl sse_servers- als auch stdio_servers-Arrays enthalten. Weitere Konfigurationsdetails und Integrationsbeispiele finden Sie in der Dokumentation.", - "uk": "Відредагуйте JSON-конфігурацію для серверів MCP нижче. Конфігурація повинна включати масиви sse_servers та stdio_servers. Повну інформацію про конфігурацію та приклади інтеграції дивіться в документації." + "uk": "Відредагуйте JSON-конфігурацію для серверів MCP нижче. Конфігурація повинна включати масиви sse_servers та stdio_servers. Повну інформацію про конфігурацію та приклади інтеграції дивіться в документації.", + "ca": "Editeu la configuració JSON per als servidors MCP a continuació. La configuració ha d'incloure els arrays sse_servers i stdio_servers. Per a informació completa sobre la configuració i exemples d'integració, consulteu la documentació." }, "SETTINGS$MCP_CONFIG_ERROR": { "en": "Error:", @@ -1085,7 +1152,8 @@ "fr": "Erreur :", "tr": "Hata:", "de": "Fehler:", - "uk": "Помилка:" + "uk": "Помилка:", + "ca": "Error:" }, "SETTINGS$MCP_CONFIG_EXAMPLE": { "en": "Example:", @@ -1101,7 +1169,8 @@ "fr": "Exemple :", "tr": "Örnek:", "de": "Beispiel:", - "uk": "Приклад:" + "uk": "Приклад:", + "ca": "Exemple:" }, "SETTINGS$MCP_NO_SERVERS_CONFIGURED": { "en": "No MCP servers are currently configured. Click \"Edit Configuration\" to add servers.", @@ -1117,7 +1186,8 @@ "fr": "Aucun serveur MCP n'est actuellement configuré. Cliquez sur \"Modifier la configuration\" pour ajouter des serveurs.", "tr": "Şu anda yapılandırılmış MCP sunucusu yok. Sunucu eklemek için \"Yapılandırmayı Düzenle\"yi tıklayın.", "de": "Derzeit sind keine MCP-Server konfiguriert. Klicken Sie auf \"Konfiguration bearbeiten\", um Server hinzuzufügen.", - "uk": "Наразі не налаштовано жодного сервера MCP. Натисніть \"Редагувати налаштування\", щоб додати сервери." + "uk": "Наразі не налаштовано жодного сервера MCP. Натисніть \"Редагувати налаштування\", щоб додати сервери.", + "ca": "No hi ha cap servidor MCP configurat. Feu clic a \"Edita la configuració\" per afegir servidors." }, "SETTINGS$MCP_SSE_SERVERS": { "en": "SSE Servers", @@ -1133,7 +1203,8 @@ "fr": "Serveurs SSE", "tr": "SSE Sunucuları", "de": "SSE-Server", - "uk": "Сервери SSE" + "uk": "Сервери SSE", + "ca": "Servidors SSE" }, "SETTINGS$MCP_STDIO_SERVERS": { "en": "Stdio Servers", @@ -1149,7 +1220,8 @@ "fr": "Serveurs Stdio", "tr": "Stdio Sunucuları", "de": "Stdio-Server", - "uk": "Сервери Stdio" + "uk": "Сервери Stdio", + "ca": "Servidors Stdio" }, "SETTINGS$MCP_API_KEY": { "en": "API Key", @@ -1165,7 +1237,8 @@ "fr": "Clé API", "tr": "API Anahtarı", "de": "API-Schlüssel", - "uk": "API-ключ" + "uk": "API-ключ", + "ca": "Clau d'API" }, "SETTINGS$MCP_API_KEY_NOT_SET": { "en": "Not set", @@ -1181,7 +1254,8 @@ "fr": "Non défini", "tr": "Ayarlanmadı", "de": "Nicht festgelegt", - "uk": "Не встановлено" + "uk": "Не встановлено", + "ca": "No establert" }, "SETTINGS$MCP_COMMAND": { "en": "Command", @@ -1197,7 +1271,8 @@ "fr": "Commande", "tr": "Komut", "de": "Befehl", - "uk": "Команда" + "uk": "Команда", + "ca": "Comanda" }, "SETTINGS$MCP_ARGS": { "en": "Args", @@ -1213,7 +1288,8 @@ "fr": "Arguments", "tr": "Argümanlar", "de": "Argumente", - "uk": "Аргументи" + "uk": "Аргументи", + "ca": "Arguments" }, "SETTINGS$MCP_ENV": { "en": "Env", @@ -1229,7 +1305,8 @@ "fr": "Environnement", "tr": "Ortam", "de": "Umgebung", - "uk": "Середовище" + "uk": "Середовище", + "ca": "Entorn" }, "SETTINGS$MCP_NAME": { "en": "Name", @@ -1245,7 +1322,8 @@ "fr": "Nom", "tr": "İsim", "de": "Name", - "uk": "Назва" + "uk": "Назва", + "ca": "Nom" }, "SETTINGS$MCP_URL": { "en": "URL", @@ -1261,7 +1339,8 @@ "fr": "URL", "tr": "URL", "de": "URL", - "uk": "URL" + "uk": "URL", + "ca": "URL" }, "SETTINGS$MCP_LEARN_MORE": { "en": "Learn more", @@ -1277,7 +1356,8 @@ "fr": "En savoir plus", "tr": "Daha fazla bilgi", "de": "Mehr erfahren", - "uk": "Дізнайтеся більше" + "uk": "Дізнайтеся більше", + "ca": "Aprèn-ne més" }, "SETTINGS$MCP_ERROR_SSE_ARRAY": { "en": "sse_servers must be an array", @@ -1293,7 +1373,8 @@ "fr": "sse_servers doit être un tableau", "tr": "sse_servers bir dizi olmalıdır", "de": "sse_servers muss ein Array sein", - "uk": "sse_servers повинна бути масивом" + "uk": "sse_servers повинна бути масивом", + "ca": "sse_servers ha de ser un array" }, "SETTINGS$MCP_ERROR_STDIO_ARRAY": { "en": "stdio_servers must be an array", @@ -1309,7 +1390,8 @@ "fr": "stdio_servers doit être un tableau", "tr": "stdio_servers bir dizi olmalıdır", "de": "stdio_servers muss ein Array sein", - "uk": "stdio_servers повинна бути масивом" + "uk": "stdio_servers повинна бути масивом", + "ca": "stdio_servers ha de ser un array" }, "SETTINGS$MCP_ERROR_SSE_URL": { "en": "Each SSE server must be a string URL or have a url property", @@ -1325,7 +1407,8 @@ "fr": "Chaque serveur SSE doit être une URL de chaîne ou avoir une propriété url", "tr": "Her SSE sunucusu bir dize URL'si olmalı veya bir url özelliğine sahip olmalıdır", "de": "Jeder SSE-Server muss eine String-URL sein oder eine URL-Eigenschaft haben", - "uk": "Кожний SSE сервер повинен бути строкою URL або мати url властивість" + "uk": "Кожний SSE сервер повинен бути строкою URL або мати url властивість", + "ca": "Cada servidor SSE ha de ser una URL de cadena o tenir una propietat url" }, "SETTINGS$MCP_ERROR_STDIO_PROPS": { "en": "Each stdio server must have name and command properties", @@ -1341,7 +1424,8 @@ "fr": "Chaque serveur stdio doit avoir les propriétés name et command", "tr": "Her stdio sunucusu name ve command özelliklerine sahip olmalıdır", "de": "Jeder stdio-Server muss die Eigenschaften name und command haben", - "uk": "Кожний stdio сервер повинен мати ім'я та командні властивості" + "uk": "Кожний stdio сервер повинен мати ім'я та командні властивості", + "ca": "Cada servidor stdio ha de tenir les propietats name i command" }, "SETTINGS$MCP_ERROR_INVALID_JSON": { "en": "Invalid JSON", @@ -1357,7 +1441,8 @@ "fr": "JSON invalide", "tr": "Geçersiz JSON", "de": "Ungültiges JSON", - "uk": "Невірний JSON" + "uk": "Невірний JSON", + "ca": "JSON no vàlid" }, "HOME$CONNECT_PROVIDER_MESSAGE": { "en": "To get started with suggested tasks, please connect your GitHub, GitLab, Bitbucket, or Azure DevOps account.", @@ -1373,7 +1458,8 @@ "fr": "Pour commencer avec les tâches suggérées, veuillez connecter votre compte GitHub, GitLab, Bitbucket ou Azure DevOps.", "tr": "Önerilen görevlerle başlamak için lütfen GitHub, GitLab, Bitbucket veya Azure DevOps hesabınızı bağlayın.", "de": "Um mit vorgeschlagenen Aufgaben zu beginnen, verbinden Sie bitte Ihr GitHub-, GitLab-, Bitbucket- oder Azure DevOps-Konto.", - "uk": "Щоб розпочати роботу з запропонованими завданнями, підключіть свій обліковий запис GitHub, GitLab, Bitbucket або Azure DevOps." + "uk": "Щоб розпочати роботу з запропонованими завданнями, підключіть свій обліковий запис GitHub, GitLab, Bitbucket або Azure DevOps.", + "ca": "Per començar amb les tasques suggerides, connecteu el vostre compte de GitHub, GitLab, Bitbucket o Azure DevOps." }, "HOME$LETS_START_BUILDING": { "en": "Let's Start Building!", @@ -1389,7 +1475,8 @@ "fr": "Commençons à construire !", "tr": "Hadi İnşa Etmeye Başlayalım!", "de": "Lass uns anfangen zu bauen!", - "uk": "Почнімо будувати!" + "uk": "Почнімо будувати!", + "ca": "Comencem a construir!" }, "HOME$OPENHANDS_DESCRIPTION": { "en": "OpenHands makes it easy to build and maintain software using AI-driven development.", @@ -1405,7 +1492,8 @@ "fr": "OpenHands facilite la création et la maintenance de logiciels grâce au développement piloté par l'IA.", "tr": "OpenHands, yapay zeka destekli geliştirme kullanarak yazılım oluşturmayı ve sürdürmeyi kolaylaştırır.", "de": "OpenHands macht es einfach, Software mit KI-gesteuerter Entwicklung zu erstellen und zu warten.", - "uk": "OpenHands спрощує створення та підтримку програмного забезпечення за допомогою розробки на основі штучного інтелекту." + "uk": "OpenHands спрощує створення та підтримку програмного забезпечення за допомогою розробки на основі штучного інтелекту.", + "ca": "OpenHands facilita la construcció i el manteniment de programari mitjançant el desenvolupament impulsat per IA." }, "HOME$NOT_SURE_HOW_TO_START": { "en": "Not sure how to start?", @@ -1421,7 +1509,8 @@ "fr": "Vous ne savez pas par où commencer ?", "tr": "Nasıl başlayacağınızdan emin değil misiniz?", "de": "Nicht sicher, wie man anfängt?", - "uk": "Не знаєте, як почати?" + "uk": "Не знаєте, як почати?", + "ca": "No esteu segur de com començar?" }, "HOME$CONNECT_TO_REPOSITORY": { "en": "Connect to a Repository", @@ -1437,7 +1526,8 @@ "fr": "Se connecter à un dépôt", "tr": "Bir Depoya Bağlan", "de": "Mit einem Repository verbinden", - "uk": "Підключіть до репозиторій" + "uk": "Підключіть до репозиторій", + "ca": "Connecta a un repositori" }, "HOME$CONNECT_TO_REPOSITORY_TOOLTIP": { "en": "You can enter a public GitHub URL if you'd like to work from a public repo instead", @@ -1453,7 +1543,8 @@ "fr": "Vous pouvez saisir une URL GitHub publique si vous souhaitez travailler à partir d'un dépôt public", "tr": "Bunun yerine genel bir repo'dan çalışmak istiyorsanız genel bir GitHub URL'si girebilirsiniz", "de": "Sie können eine öffentliche GitHub-URL eingeben, wenn Sie stattdessen von einem öffentlichen Repository arbeiten möchten", - "uk": "Ви можете ввести публічну GitHub URL, якщо хочете працювати з публічного репозиторію" + "uk": "Ви можете ввести публічну GitHub URL, якщо хочете працювати з публічного репозиторію", + "ca": "Podeu introduir una URL pública de GitHub si preferiu treballar des d'un repositori públic" }, "HOME$LOADING": { "en": "Loading...", @@ -1469,7 +1560,8 @@ "fr": "Chargement...", "tr": "Yükleniyor...", "de": "Wird geladen...", - "uk": "Завантаження..." + "uk": "Завантаження...", + "ca": "Carregant..." }, "HOME$LOADING_REPOSITORIES": { "en": "Loading repositories...", @@ -1485,7 +1577,8 @@ "fr": "Chargement des dépôts...", "tr": "Depolar yükleniyor...", "de": "Repositories werden geladen...", - "uk": "Завантаження репозиторіїв..." + "uk": "Завантаження репозиторіїв...", + "ca": "Carregant repositoris..." }, "HOME$SEARCHING_REPOSITORIES": { "en": "Searching repositories...", @@ -1501,7 +1594,8 @@ "fr": "Recherche de dépôts...", "tr": "Depolar aranıyor...", "de": "Repositories werden durchsucht...", - "uk": "Пошук репозиторіїв..." + "uk": "Пошук репозиторіїв...", + "ca": "Cercant repositoris..." }, "HOME$LOADING_MORE_REPOSITORIES": { "en": "Loading more repositories...", @@ -1517,7 +1611,8 @@ "fr": "Chargement de plus de dépôts...", "tr": "Daha fazla depolar yükleniyor...", "de": "Weitere Repositories werden geladen...", - "uk": "Завантаження більше репозиторіїв..." + "uk": "Завантаження більше репозиторіїв...", + "ca": "Carregant més repositoris..." }, "HOME$FAILED_TO_LOAD_REPOSITORIES": { "en": "Failed to load repositories", @@ -1533,7 +1628,8 @@ "fr": "Échec du chargement des dépôts", "tr": "Depolar yüklenemedi", "de": "Fehler beim Laden der Repositories", - "uk": "Не вдалося завантажити репозиторії" + "uk": "Не вдалося завантажити репозиторії", + "ca": "No s'han pogut carregar els repositoris" }, "HOME$LOADING_BRANCHES": { "en": "Loading branches...", @@ -1549,7 +1645,8 @@ "fr": "Chargement des branches...", "tr": "Dallar yükleniyor...", "de": "Lade Branches...", - "uk": "Завантаження гілок..." + "uk": "Завантаження гілок...", + "ca": "Carregant branques..." }, "HOME$FAILED_TO_LOAD_BRANCHES": { "en": "Failed to load branches", @@ -1565,7 +1662,8 @@ "fr": "Échec du chargement des branches", "tr": "Dallar yüklenemedi", "de": "Fehler beim Laden der Branches", - "uk": "Не вдалося завантажити гілки" + "uk": "Не вдалося завантажити гілки", + "ca": "No s'han pogut carregar les branques" }, "HOME$OPEN_ISSUE": { "en": "Open issue", @@ -1581,7 +1679,8 @@ "fr": "Problème ouvert", "tr": "Açık sorun", "de": "Offenes Problem", - "uk": "Повідомити про проблему" + "uk": "Повідомити про проблему", + "ca": "Problema obert" }, "HOME$FIX_FAILING_CHECKS": { "en": "Fix failing checks", @@ -1597,7 +1696,8 @@ "fr": "Corriger les vérifications échouées", "tr": "Başarısız kontrolleri düzelt", "de": "Fehlgeschlagene Prüfungen beheben", - "uk": "Виправити невдалі перевірки" + "uk": "Виправити невдалі перевірки", + "ca": "Corregeix les comprovacions fallides" }, "HOME$RESOLVE_MERGE_CONFLICTS": { "en": "Resolve merge conflicts", @@ -1613,7 +1713,8 @@ "fr": "Résoudre les conflits de fusion", "tr": "Birleştirme çakışmalarını çöz", "de": "Merge-Konflikte lösen", - "uk": "Вирішити конфлікти злиття" + "uk": "Вирішити конфлікти злиття", + "ca": "Resol els conflictes de fusió" }, "HOME$RESOLVE_UNRESOLVED_COMMENTS": { "en": "Resolve unresolved comments", @@ -1629,7 +1730,8 @@ "fr": "Résoudre les commentaires non résolus", "tr": "Çözülmemiş yorumları çöz", "de": "Ungelöste Kommentare beheben", - "uk": "Вирішити невирішені коментарі" + "uk": "Вирішити невирішені коментарі", + "ca": "Resol els comentaris no resolts" }, "HOME$LAUNCH": { "en": "Launch", @@ -1645,7 +1747,8 @@ "fr": "Lancer", "tr": "Başlat", "de": "Starten", - "uk": "Запуск" + "uk": "Запуск", + "ca": "Llança" }, "SETTINGS$ADVANCED": { "en": "Advanced", @@ -1661,7 +1764,8 @@ "fr": "Avancé", "tr": "Gelişmiş", "de": "Erweitert", - "uk": "Розширений" + "uk": "Розширений", + "ca": "Avançat" }, "SETTINGS$BASE_URL": { "en": "Base URL", @@ -1677,7 +1781,8 @@ "fr": "URL de base", "tr": "Temel URL", "de": "Basis-URL", - "uk": "Базовий URL" + "uk": "Базовий URL", + "ca": "URL base" }, "SETTINGS$AGENT": { "en": "Agent", @@ -1693,7 +1798,8 @@ "fr": "Agent", "tr": "Ajan", "de": "Agent", - "uk": "Агент" + "uk": "Агент", + "ca": "Agent" }, "SETTINGS$ENABLE_MEMORY_CONDENSATION": { "en": "Enable memory condensation", @@ -1709,7 +1815,8 @@ "fr": "Activer la condensation de mémoire", "tr": "Bellek yoğunlaştırmayı etkinleştir", "de": "Speicherkondensation aktivieren", - "uk": "Увімкнути конденсацію пам'яті" + "uk": "Увімкнути конденсацію пам'яті", + "ca": "Activa la condensació de memòria" }, "SETTINGS$CONDENSER_MAX_SIZE": { "en": "Memory condenser max history size", @@ -1725,7 +1832,8 @@ "fr": "Taille maximale de l'historique du condenseur de mémoire", "tr": "Bellek yoğunlaştırıcı maksimum geçmiş boyutu", "de": "Maximale Verlaufgröße des Speicherkondensators", - "uk": "Максимальний розмір історії конденсатора пам'яті" + "uk": "Максимальний розмір історії конденсатора пам'яті", + "ca": "Mida màxima de l'historial del condensador de memòria" }, "SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP": { "en": "After this many events, the condenser will summarize history. Minimum 20.", @@ -1741,7 +1849,8 @@ "fr": "Après ce nombre d'événements, le condenseur résumera l'historique. Minimum 20.", "tr": "Bu kadar olaydan sonra yoğunlaştırıcı geçmişi özetler. En az 20.", "de": "Nach so vielen Ereignissen fasst der Kondensator die Historie zusammen. Minimum 20.", - "uk": "Після цієї кількості подій конденсатор узагальнить історію. Мінімум 20." + "uk": "Після цієї кількості подій конденсатор узагальнить історію. Мінімум 20.", + "ca": "Després d'aquest nombre d'esdeveniments, el condensador resumirà l'historial. Mínim 20." }, "SETTINGS$LANGUAGE": { "en": "Language", @@ -1757,7 +1866,8 @@ "fr": "Langue", "tr": "Dil", "de": "Sprache", - "uk": "Мова" + "uk": "Мова", + "ca": "Idioma" }, "ACTION$PUSH_TO_BRANCH": { "en": "Push to Branch", @@ -1773,7 +1883,8 @@ "fr": "Pousser vers la branche", "tr": "Dala İtme", "de": "Zum Branch pushen", - "uk": "Надіслати у гілку" + "uk": "Надіслати у гілку", + "ca": "Publica a la branca" }, "ACTION$PUSH_CREATE_PR": { "en": "Push & Create PR", @@ -1789,7 +1900,8 @@ "fr": "Pousser et créer une PR", "tr": "İtme ve PR Oluştur", "de": "Pushen & PR erstellen", - "uk": "Надіслати & Створити PR" + "uk": "Надіслати & Створити PR", + "ca": "Publica i crea PR" }, "ACTION$PUSH_CHANGES_TO_PR": { "en": "Push changes to PR", @@ -1805,7 +1917,8 @@ "fr": "Pousser les modifications vers la PR", "tr": "Değişiklikleri PR'a İtme", "de": "Änderungen zum PR pushen", - "uk": "Надіслати зміти в PR" + "uk": "Надіслати зміти в PR", + "ca": "Publica els canvis a la PR" }, "ANALYTICS$TITLE": { "en": "Your Privacy Preferences", @@ -1821,7 +1934,8 @@ "fr": "Vos préférences de confidentialité", "tr": "Gizlilik Tercihleriniz", "de": "Ihre Datenschutzeinstellungen", - "uk": "Ваші налаштування конфіденційності" + "uk": "Ваші налаштування конфіденційності", + "ca": "Les vostres preferències de privadesa" }, "ANALYTICS$DESCRIPTION": { "en": "We use tools to understand how our application is used to improve your experience. You can enable or disable analytics. Your preferences will be stored and can be updated anytime.", @@ -1837,7 +1951,8 @@ "fr": "Nous utilisons des outils pour comprendre comment notre application est utilisée afin d'améliorer votre expérience. Vous pouvez activer ou désactiver les analyses. Vos préférences seront stockées et peuvent être mises à jour à tout moment.", "tr": "Uygulamamızın deneyiminizi geliştirmek için nasıl kullanıldığını anlamak için araçlar kullanıyoruz. Analitiği etkinleştirebilir veya devre dışı bırakabilirsiniz. Tercihleriniz saklanacak ve istediğiniz zaman güncellenebilir.", "de": "Wir verwenden Tools, um zu verstehen, wie unsere Anwendung genutzt wird, um Ihre Erfahrung zu verbessern. Sie können Analysen aktivieren oder deaktivieren. Ihre Einstellungen werden gespeichert und können jederzeit aktualisiert werden.", - "uk": "Ми використовуємо інструменти, щоб зрозуміти, як використовується наш додаток, для покращення вашого досвіду. Ви можете ввімкнути або вимкнути аналітику. Ваші налаштування будуть збережені та можуть бути оновлені будь-коли." + "uk": "Ми використовуємо інструменти, щоб зрозуміти, як використовується наш додаток, для покращення вашого досвіду. Ви можете ввімкнути або вимкнути аналітику. Ваші налаштування будуть збережені та можуть бути оновлені будь-коли.", + "ca": "Fem servir eines per entendre com s'utilitza la nostra aplicació i millorar la vostra experiència. Podeu activar o desactivar les analítiques. Les vostres preferències es desaran i es podran actualitzar en qualsevol moment." }, "ANALYTICS$SEND_ANONYMOUS_DATA": { "en": "Send anonymous usage data", @@ -1853,7 +1968,8 @@ "fr": "Envoyer des données d'utilisation anonymes", "tr": "Anonim kullanım verilerini gönder", "de": "Anonyme Nutzungsdaten senden", - "uk": "Надсилати анонімні дані про використання" + "uk": "Надсилати анонімні дані про використання", + "ca": "Envia dades d'ús anònimes" }, "ANALYTICS$CONFIRM_PREFERENCES": { "en": "Confirm Preferences", @@ -1869,7 +1985,8 @@ "fr": "Confirmer les préférences", "tr": "Tercihleri Onayla", "de": "Einstellungen bestätigen", - "uk": "Підтвердити налаштування" + "uk": "Підтвердити налаштування", + "ca": "Confirma les preferències" }, "SETTINGS$SAVING": { "en": "Saving...", @@ -1885,7 +2002,8 @@ "fr": "Enregistrement en cours...", "tr": "Kayıt yapılıyor...", "de": "Speichern...", - "uk": "Зберігаю..." + "uk": "Зберігаю...", + "ca": "Desant..." }, "SETTINGS$SAVE_CHANGES": { "en": "Save Changes", @@ -1901,7 +2019,8 @@ "fr": "Enregistrer les modifications", "tr": "Değişiklikleri Kaydet", "de": "Änderungen speichern", - "uk": "Зберегти зміни" + "uk": "Зберегти зміни", + "ca": "Desa els canvis" }, "SETTINGS$NAV_INTEGRATIONS": { "en": "Integrations", @@ -1917,7 +2036,8 @@ "fr": "Intégrations", "tr": "Entegrasyonlar", "de": "Integrationen", - "uk": "Інтеграції" + "uk": "Інтеграції", + "ca": "Integracions" }, "SETTINGS$NAV_APPLICATION": { "en": "Application", @@ -1933,7 +2053,8 @@ "fr": "Application", "tr": "Uygulama", "de": "Anwendung", - "uk": "Додаток" + "uk": "Додаток", + "ca": "Aplicació" }, "SETTINGS$NAV_BILLING": { "en": "Billing", @@ -1949,7 +2070,8 @@ "fr": "Facturation", "tr": "Faturalama", "de": "Abrechnung", - "uk": "Виставлення рахунків" + "uk": "Виставлення рахунків", + "ca": "Facturació" }, "SETTINGS$NAV_SECRETS": { "en": "Secrets", @@ -1965,7 +2087,8 @@ "fr": "Secrets", "tr": "Sırları", "de": "Geheimnisse", - "uk": "Секрети" + "uk": "Секрети", + "ca": "Secrets" }, "SETTINGS$NAV_API_KEYS": { "en": "API Keys", @@ -1981,7 +2104,8 @@ "fr": "Clés API", "tr": "API Anahtarları", "de": "API-Schlüssel", - "uk": "API ключі" + "uk": "API ключі", + "ca": "Claus d'API" }, "SETTINGS$GITHUB": { "en": "GitHub", @@ -1997,7 +2121,8 @@ "fr": "GitHub", "tr": "GitHub", "de": "GitHub", - "uk": "GitHub" + "uk": "GitHub", + "ca": "GitHub" }, "SETTINGS$AZURE_DEVOPS": { "en": "Azure DevOps", @@ -2013,7 +2138,8 @@ "fr": "Azure DevOps", "tr": "Azure DevOps", "de": "Azure DevOps", - "uk": "Azure DevOps" + "uk": "Azure DevOps", + "ca": "Azure DevOps" }, "SETTINGS$SLACK": { "en": "Slack", @@ -2029,7 +2155,8 @@ "fr": "Slack", "tr": "Slack", "de": "Slack", - "uk": "Slack" + "uk": "Slack", + "ca": "Slack" }, "COMMON$STATUS": { "en": "Status", @@ -2045,7 +2172,8 @@ "fr": "Statut", "tr": "Durum", "de": "Status", - "uk": "Статус" + "uk": "Статус", + "ca": "Estat" }, "SETTINGS$GITLAB_NOT_CONNECTED": { "en": "Not Connected", @@ -2061,7 +2189,8 @@ "fr": "Non connecté", "tr": "Bağlı değil", "de": "Nicht verbunden", - "uk": "Не підключено" + "uk": "Не підключено", + "ca": "No connectat" }, "SETTINGS$GITLAB_REINSTALL_WEBHOOK": { "en": "Reinstall Webhook", @@ -2077,7 +2206,8 @@ "fr": "Réinstaller le Webhook", "tr": "Webhook'u Yeniden Kur", "de": "Webhook neu installieren", - "uk": "Перевстановити Webhook" + "uk": "Перевстановити Webhook", + "ca": "Reinstal·la el Webhook" }, "SETTINGS$GITLAB_INSTALLING_WEBHOOK": { "en": "Installing GitLab webhook, please wait a few minutes.", @@ -2093,7 +2223,8 @@ "fr": "Installation du webhook GitLab, veuillez patienter quelques minutes.", "tr": "GitLab webhook'u yükleniyor, lütfen birkaç dakika bekleyin.", "de": "GitLab-Webhook wird installiert. Bitte warten Sie einige Minuten.", - "uk": "Встановлення GitLab webhook, зачекайте кілька хвилин." + "uk": "Встановлення GitLab webhook, зачекайте кілька хвилин.", + "ca": "S'està instal·lant el webhook de GitLab, espereu uns minuts." }, "SETTINGS$GITLAB": { "en": "GitLab", @@ -2109,7 +2240,8 @@ "fr": "GitLab", "tr": "GitLab", "de": "GitLab", - "uk": "GitLab" + "uk": "GitLab", + "ca": "GitLab" }, "SETTINGS$NAV_LLM": { "en": "LLM", @@ -2125,7 +2257,8 @@ "fr": "LLM", "tr": "LLM", "de": "LLM", - "uk": "LLM" + "uk": "LLM", + "ca": "LLM" }, "GIT$MERGE_REQUEST": { "en": "Merge Request", @@ -2141,7 +2274,8 @@ "fr": "Demande de fusion", "tr": "Birleştirme İsteği", "de": "Merge-Anfrage", - "uk": "Запит на злиття" + "uk": "Запит на злиття", + "ca": "Sol·licitud de fusió" }, "GIT$GITLAB_API": { "en": "GitLab API", @@ -2157,7 +2291,8 @@ "fr": "API GitLab", "tr": "GitLab API", "de": "GitLab API", - "uk": "GitLab API" + "uk": "GitLab API", + "ca": "API de GitLab" }, "GIT$PULL_REQUEST": { "en": "Pull Request", @@ -2173,7 +2308,8 @@ "fr": "Demande de tirage", "tr": "Çekme İsteği", "de": "Pull Request", - "uk": "Запит на злиття" + "uk": "Запит на злиття", + "ca": "Sol·licitud de canvis" }, "GIT$GITHUB_API": { "en": "GitHub API", @@ -2189,7 +2325,8 @@ "fr": "API GitHub", "tr": "GitHub API", "de": "GitHub API", - "uk": "GitHub API" + "uk": "GitHub API", + "ca": "API de GitHub" }, "BUTTON$COPY": { "en": "Copy to clipboard", @@ -2205,7 +2342,8 @@ "fr": "Copier dans le presse-papiers", "tr": "Panoya kopyala", "de": "In die Zwischenablage kopieren", - "uk": "Копіювати в буфер обміну" + "uk": "Копіювати в буфер обміну", + "ca": "Copia al porta-retalls" }, "BUTTON$COPIED": { "en": "Copied to clipboard", @@ -2221,7 +2359,8 @@ "fr": "Copié dans le presse-papiers", "tr": "Panoya kopyalandı", "de": "In die Zwischenablage kopiert", - "uk": "Copied to clipboard" + "uk": "Copied to clipboard", + "ca": "Copiat al porta-retalls" }, "APP$TITLE": { "en": "App", @@ -2237,7 +2376,8 @@ "fr": "App", "tr": "Uygulama", "de": "App", - "uk": "App" + "uk": "App", + "ca": "Aplicació" }, "BROWSER$TITLE": { "en": "Browser", @@ -2253,7 +2393,8 @@ "fr": "Navigateur", "tr": "Tarayıcı", "de": "Browser", - "uk": "Браузер" + "uk": "Браузер", + "ca": "Navegador" }, "BROWSER$EMPTY_MESSAGE": { "en": "If you tell OpenHands to start a web server, the app will appear here.", @@ -2269,7 +2410,8 @@ "fr": "Si vous demandez à OpenHands de démarrer un serveur web, l'application apparaîtra ici.", "tr": "OpenHands'e bir web sunucusu başlatmasını söylerseniz, uygulama burada görünecektir.", "de": "Wenn Sie OpenHands anweisen, einen Webserver zu starten, erscheint die App hier.", - "uk": "Якщо ви накажете OpenHands запустити вебсервер, програма з'явиться тут." + "uk": "Якщо ви накажете OpenHands запустити вебсервер, програма з'явиться тут.", + "ca": "Si li dieu a OpenHands que iniciï un servidor web, l'aplicació apareixerà aquí." }, "SETTINGS$TITLE": { "en": "Settings", @@ -2285,7 +2427,8 @@ "fr": "Paramètres", "tr": "Ayarlar", "de": "Einstellungen", - "uk": "Налаштування" + "uk": "Налаштування", + "ca": "Configuració" }, "CONVERSATION$START_NEW": { "en": "Start new conversation", @@ -2301,7 +2444,8 @@ "fr": "Démarrer une nouvelle conversation", "tr": "Yeni sohbet başlat", "de": "Neue Unterhaltung starten", - "uk": "Почати нову розмову" + "uk": "Почати нову розмову", + "ca": "Inicia una conversa nova" }, "CONVERSATION$REPOSITORY": { "en": "Repository", @@ -2317,7 +2461,8 @@ "fr": "Dépôt", "tr": "Depo", "de": "Repository", - "uk": "Репозиторій" + "uk": "Репозиторій", + "ca": "Repositori" }, "CONVERSATION$BRANCH": { "en": "Branch", @@ -2333,7 +2478,8 @@ "fr": "Branche", "tr": "Dal", "de": "Zweig", - "uk": "Гілка" + "uk": "Гілка", + "ca": "Branca" }, "CONVERSATION$GIT_PROVIDER": { "en": "Git Provider", @@ -2349,7 +2495,8 @@ "fr": "Fournisseur Git", "tr": "Git Sağlayıcısı", "de": "Git-Anbieter", - "uk": "Git-провайдер" + "uk": "Git-провайдер", + "ca": "Proveïdor de Git" }, "WORKSPACE$TERMINAL_TAB_LABEL": { "en": "Terminal", @@ -2365,7 +2512,8 @@ "fr": "Terminal", "tr": "Terminal", "ja": "ターミナル", - "uk": "Термінал" + "uk": "Термінал", + "ca": "Terminal" }, "WORKSPACE$BROWSER_TAB_LABEL": { "en": "Browser", @@ -2381,7 +2529,8 @@ "fr": "Navigateur", "tr": "Tarayıcı", "ja": "ブラウザ", - "uk": "Браузер" + "uk": "Браузер", + "ca": "Navegador" }, "WORKSPACE$JUPYTER_TAB_LABEL": { "en": "Jupyter", @@ -2397,7 +2546,8 @@ "fr": "Jupyter", "tr": "Jupyter", "ja": "Jupyter", - "uk": "Jupyter" + "uk": "Jupyter", + "ca": "Jupyter" }, "WORKSPACE$CODE_EDITOR_TAB_LABEL": { "en": "Code Editor", @@ -2413,7 +2563,8 @@ "fr": "Éditeur de code", "tr": "Kod Düzenleyici", "ja": "コードエディタ", - "uk": "Редактор коду" + "uk": "Редактор коду", + "ca": "Editor de codi" }, "WORKSPACE$TITLE": { "en": "Workspace", @@ -2429,7 +2580,8 @@ "fr": "Espace de travail", "tr": "Çalışma Alanı", "ja": "ワークスペース", - "uk": "Робочий простір" + "uk": "Робочий простір", + "ca": "Espai de treball" }, "TERMINAL$WAITING_FOR_CLIENT": { "en": "Waiting for client to become ready...", @@ -2445,7 +2597,8 @@ "ar": "في انتظار جاهزية العميل...", "fr": "En attente de la disponibilité du client...", "tr": "İstemcinin hazır olması bekleniyor...", - "uk": "Чекаємо на готовність клієнта..." + "uk": "Чекаємо на готовність клієнта...", + "ca": "Esperant que el client estigui preparat..." }, "CODE_EDITOR$FILE_SAVED_SUCCESSFULLY": { "en": "File saved successfully", @@ -2461,7 +2614,8 @@ "fr": "Fichier enregistré avec succès", "tr": "Dosya başarıyla kaydedildi", "ja": "ファイルが正常に保存されました", - "uk": "Файл успішно збережено" + "uk": "Файл успішно збережено", + "ca": "El fitxer s'ha desat correctament" }, "CODE_EDITOR$SAVING_LABEL": { "en": "Saving...", @@ -2477,7 +2631,8 @@ "fr": "Enregistrement en cours...", "tr": "Kayıt yapılıyor...", "ja": "保存中...", - "uk": "Збереження..." + "uk": "Збереження...", + "ca": "Desant..." }, "CODE_EDITOR$SAVE_LABEL": { "en": "Save", @@ -2493,7 +2648,8 @@ "fr": "Enregistrer", "tr": "Kayıt", "ja": "保存", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "CODE_EDITOR$OPTIONS": { "en": "Options", @@ -2509,7 +2665,8 @@ "fr": "Options", "tr": "Seçenekler", "ja": "オプション", - "uk": "Опції" + "uk": "Опції", + "ca": "Opcions" }, "CODE_EDITOR$FILE_SAVE_ERROR": { "en": "An unknown error occurred while saving the file", @@ -2525,7 +2682,8 @@ "fr": "Une erreur inconnue s'est produite lors de l'enregistrement du fichier", "tr": "Dosya kaydedilirken bilinmeyen bir hata oluştu", "ja": "ファイルの保存中に不明なエラーが発生しました", - "uk": "Під час збереження файлу сталася невідома помилка" + "uk": "Під час збереження файлу сталася невідома помилка", + "ca": "S'ha produït un error desconegut en desar el fitxer" }, "CODE_EDITOR$EMPTY_MESSAGE": { "en": "No file selected.", @@ -2541,7 +2699,8 @@ "fr": "Aucun fichier sélectionné.", "tr": "Hiçbir dosya seçilmedi.", "ja": "ファイルが選択されていません。", - "uk": "Файл не вибрано." + "uk": "Файл не вибрано.", + "ca": "Cap fitxer seleccionat." }, "FILE_SERVICE$SELECT_FILE_ERROR": { "en": "Error selecting file. Please try again.", @@ -2557,7 +2716,8 @@ "fr": "Erreur lors de la sélection du fichier. Veuillez réessayer.", "tr": "Dosya seçiminde hata oluştu. Lütfen tekrar deneyin.", "ja": "ファイルの選択中にエラーが発生しました。もう一度お試しください。", - "uk": "Помилка вибору файлу. Спробуйте ще раз." + "uk": "Помилка вибору файлу. Спробуйте ще раз.", + "ca": "Error en seleccionar el fitxer. Torneu-ho a intentar." }, "FILE_SERVICE$UPLOAD_FILES_ERROR": { "en": "Error uploading files. Please try again.", @@ -2573,7 +2733,8 @@ "fr": "Erreur lors du téléchargement des fichiers. Veuillez réessayer.", "tr": "Dosyalar yüklenirken hata oluştu. Lütfen tekrar deneyin.", "ja": "ファイルのアップロード中にエラーが発生しました。もう一度お試しください。", - "uk": "Помилка завантаження файлів. Спробуйте ще раз." + "uk": "Помилка завантаження файлів. Спробуйте ще раз.", + "ca": "Error en carregar els fitxers. Torneu-ho a intentar." }, "FILE_SERVICE$LIST_FILES_ERROR": { "en": "Error listing files. Please try again.", @@ -2589,7 +2750,8 @@ "fr": "Erreur lors de la liste des fichiers. Veuillez réessayer.", "tr": "Dosyalar listelenirken hata oluştu. Lütfen tekrar deneyin.", "ja": "ファイル一覧の取得中にエラーが発生しました。もう一度お試しください。", - "uk": "Error listing files. Please try again." + "uk": "Error listing files. Please try again.", + "ca": "Error en llistar els fitxers. Torneu-ho a intentar." }, "FILE_SERVICE$SAVE_FILE_ERROR": { "en": "Error saving file. Please try again.", @@ -2605,7 +2767,8 @@ "fr": "Erreur lors de l'enregistrement du fichier. Veuillez réessayer.", "tr": "Dosya kaydedilirken hata oluştu. Lütfen tekrar deneyin.", "ja": "ファイルの保存中にエラーが発生しました。もう一度お試しください。", - "uk": "Помилка збереження файлу. Спробуйте ще раз." + "uk": "Помилка збереження файлу. Спробуйте ще раз.", + "ca": "Error en desar el fitxer. Torneu-ho a intentar." }, "SUGGESTIONS$INCREASE_TEST_COVERAGE": { "en": "Increase test coverage", @@ -2621,7 +2784,8 @@ "ar": "زيادة تغطية الاختبار", "fr": "Augmenter la couverture des tests", "tr": "Test kapsamını artır", - "uk": "Збільшити охоплення тестами" + "uk": "Збільшити охоплення тестами", + "ca": "Augmenta la cobertura de proves" }, "SUGGESTIONS$AUTO_MERGE_PRS": { "en": "Auto-merge Dependabot PRs", @@ -2637,7 +2801,8 @@ "ar": "دمج تلقائي لـ PRs من Dependabot", "fr": "Fusion automatique des PR Dependabot", "tr": "Dependabot PR'larını otomatik birleştir", - "uk": "Автоматичне злиття PR" + "uk": "Автоматичне злиття PR", + "ca": "Fusió automàtica de les PR de Dependabot" }, "SUGGESTIONS$FIX_README": { "en": "Improve README", @@ -2653,7 +2818,8 @@ "ar": "تحسين README", "fr": "Améliorer le README", "tr": "README'yi geliştir", - "uk": "Покращити README" + "uk": "Покращити README", + "ca": "Millora el README" }, "SUGGESTIONS$CLEAN_DEPENDENCIES": { "en": "Clean up dependencies", @@ -2669,7 +2835,8 @@ "ar": "تنظيف التبعيات", "fr": "Nettoyer les dépendances", "tr": "Bağımlılıkları temizle", - "uk": "Очищення залежностей" + "uk": "Очищення залежностей", + "ca": "Neteja les dependències" }, "SETTINGS$LLM_SETTINGS": { "en": "LLM Settings", @@ -2685,7 +2852,8 @@ "ar": "إعدادات LLM", "fr": "Paramètres LLM", "tr": "LLM Ayarları", - "uk": "LLM налаштування" + "uk": "LLM налаштування", + "ca": "Configuració del LLM" }, "SETTINGS$GIT_SETTINGS": { "en": "Git Settings", @@ -2701,7 +2869,8 @@ "ar": "إعدادات Git", "fr": "Paramètres Git", "tr": "Git Ayarları", - "uk": "Git налаштування" + "uk": "Git налаштування", + "ca": "Configuració de Git" }, "SETTINGS$GIT_SETTINGS_DESCRIPTION": { "en": "Configure the username and email that OpenHands uses to commit changes.", @@ -2717,7 +2886,8 @@ "ar": "قم بتكوين اسم المستخدم والبريد الإلكتروني الذي يستخدمه OpenHands لارتكاب التغييرات.", "fr": "Configurez le nom d'utilisateur et l'email qu'OpenHands utilise pour valider les modifications.", "tr": "OpenHands'ın değişiklikleri commit etmek için kullandığı kullanıcı adını ve e-postayı yapılandırın.", - "uk": "Налаштуйте ім'я користувача та електронну пошту, які OpenHands використовує для фіксації змін." + "uk": "Налаштуйте ім'я користувача та електронну пошту, які OpenHands використовує для фіксації змін.", + "ca": "Configura el nom d'usuari i el correu electrònic que OpenHands utilitza per confirmar els canvis." }, "SETTINGS$SOUND_NOTIFICATIONS": { "en": "Sound Notifications", @@ -2733,7 +2903,8 @@ "ar": "إشعارات صوتية", "fr": "Notifications sonores", "tr": "Ses Bildirimleri", - "uk": "Звукові сповіщення" + "uk": "Звукові сповіщення", + "ca": "Notificacions de so" }, "SETTINGS$MAX_BUDGET_PER_TASK": { "en": "Maximum Budget Per Task", @@ -2749,7 +2920,8 @@ "ar": "الميزانية القصوى لكل مهمة", "fr": "Budget maximum par tâche", "tr": "Görev Başına Maksimum Bütçe", - "uk": "Максимальний бюджет на завдання" + "uk": "Максимальний бюджет на завдання", + "ca": "Pressupost màxim per tasca" }, "SETTINGS$MAX_BUDGET_PER_CONVERSATION": { "en": "Maximum Budget Per Conversation", @@ -2765,7 +2937,8 @@ "ar": "الميزانية القصوى لكل محادثة", "fr": "Budget maximum par conversation", "tr": "Konuşma Başına Maksimum Bütçe", - "uk": "Максимальний бюджет на розмову" + "uk": "Максимальний бюджет на розмову", + "ca": "Pressupost màxim per conversa" }, "SETTINGS$PROACTIVE_CONVERSATION_STARTERS": { "en": "Suggest Tasks on GitHub", @@ -2781,7 +2954,8 @@ "ar": "اقتراح المهام على GitHub", "fr": "Suggérer des tâches sur GitHub", "tr": "GitHub'da Görevler Öner", - "uk": "Запропонувати завдання на GitHub" + "uk": "Запропонувати завдання на GitHub", + "ca": "Suggereix tasques a GitHub" }, "SETTINGS$SOLVABILITY_ANALYSIS": { "en": "Enable Solvability Analysis", @@ -2797,7 +2971,8 @@ "ar": "تمكين تحليل القابلية للحل", "fr": "Activer l'analyse de solvabilité", "tr": "Çözünürlük Analizini Etkinleştir", - "uk": "Увімкнути аналіз розв'язності" + "uk": "Увімкнути аналіз розв'язності", + "ca": "Activa l'anàlisi de resolubilitat" }, "SETTINGS$SANDBOX_GROUPING_STRATEGY": { "en": "Sandbox Grouping Strategy", @@ -2813,7 +2988,8 @@ "ar": "استراتيجية تجميع صندوق الرمل", "fr": "Stratégie de regroupement sandbox", "tr": "Sandbox Gruplama Stratejisi", - "uk": "Стратегія групування пісочниці" + "uk": "Стратегія групування пісочниці", + "ca": "Estratègia d'agrupació de sandbox" }, "SETTINGS$SANDBOX_GROUPING_NO_GROUPING": { "en": "No Grouping (new sandbox per conversation)", @@ -2829,7 +3005,8 @@ "ar": "بدون تجميع (صندوق رمل جديد لكل محادثة)", "fr": "Pas de regroupement (nouveau sandbox par conversation)", "tr": "Gruplama Yok (konuşma başına yeni sandbox)", - "uk": "Без групування (нова пісочниця для кожної розмови)" + "uk": "Без групування (нова пісочниця для кожної розмови)", + "ca": "Sense agrupació (nou sandbox per conversa)" }, "SETTINGS$SANDBOX_GROUPING_GROUP_BY_NEWEST": { "en": "Group by Newest (add to most recent sandbox)", @@ -2845,7 +3022,8 @@ "ar": "التجميع حسب الأحدث (إضافة إلى أحدث صندوق رمل)", "fr": "Regrouper par le plus récent (ajouter au sandbox le plus récent)", "tr": "En Yeniye Göre Grupla (en yeni sandbox'a ekle)", - "uk": "Групувати за найновішим (додати до найновішої пісочниці)" + "uk": "Групувати за найновішим (додати до найновішої пісочниці)", + "ca": "Agrupa pel més nou (afegeix al sandbox més recent)" }, "SETTINGS$SANDBOX_GROUPING_LEAST_RECENTLY_USED": { "en": "Least Recently Used (add to oldest sandbox)", @@ -2861,7 +3039,8 @@ "ar": "الأقل استخدامًا مؤخرًا (إضافة إلى أقدم صندوق رمل)", "fr": "Le moins récemment utilisé (ajouter au sandbox le plus ancien)", "tr": "En Az Kullanılan (en eski sandbox'a ekle)", - "uk": "Найменш нещодавно використана (додати до найстаршої пісочниці)" + "uk": "Найменш нещодавно використана (додати до найстаршої пісочниці)", + "ca": "Menys usat recentment (afegeix al sandbox més antic)" }, "SETTINGS$SANDBOX_GROUPING_FEWEST_CONVERSATIONS": { "en": "Fewest Conversations (add to least busy sandbox)", @@ -2877,7 +3056,8 @@ "ar": "أقل محادثات (إضافة إلى صندوق الرمل الأقل انشغالاً)", "fr": "Moins de conversations (ajouter au sandbox le moins occupé)", "tr": "En Az Konuşma (en az meşgul sandbox'a ekle)", - "uk": "Найменше розмов (додати до найменш зайнятої пісочниці)" + "uk": "Найменше розмов (додати до найменш зайнятої пісочниці)", + "ca": "Menys converses (afegeix al sandbox menys ocupat)" }, "SETTINGS$SANDBOX_GROUPING_ADD_TO_ANY": { "en": "Add to Any (use first available sandbox)", @@ -2893,7 +3073,8 @@ "ar": "إضافة إلى أي (استخدام أول صندوق رمل متاح)", "fr": "Ajouter à n'importe lequel (utiliser le premier sandbox disponible)", "tr": "Herhangi Birine Ekle (ilk uygun sandbox'ı kullan)", - "uk": "Додати до будь-якої (використовувати першу доступну пісочницю)" + "uk": "Додати до будь-якої (використовувати першу доступну пісочницю)", + "ca": "Afegeix a qualsevol (usa el primer sandbox disponible)" }, "SETTINGS$SEARCH_API_KEY": { "en": "Search API Key (Tavily)", @@ -2909,7 +3090,8 @@ "ar": "مفتاح API للبحث (Tavily)", "fr": "Clé API de recherche (Tavily)", "tr": "Arama API Anahtarı (Tavily)", - "uk": "Ключ API пошуку (Tavily)" + "uk": "Ключ API пошуку (Tavily)", + "ca": "Clau d'API de cerca (Tavily)" }, "SETTINGS$SEARCH_API_KEY_OPTIONAL": { "en": "This field is optional. We use Tavily as our default search engine provider.", @@ -2925,7 +3107,8 @@ "ar": "هذا الحقل اختياري. نستخدم Tavily كمزود محرك البحث الافتراضي.", "fr": "Ce champ est facultatif. Nous utilisons Tavily comme fournisseur de moteur de recherche par défaut.", "tr": "Bu alan isteğe bağlıdır. Varsayılan arama motoru sağlayıcısı olarak Tavily'yi kullanıyoruz.", - "uk": "Це поле є необов'язковим. Ми використовуємо Tavily як нашого типового постачальника пошукової системи." + "uk": "Це поле є необов'язковим. Ми використовуємо Tavily як нашого типового постачальника пошукової системи.", + "ca": "Aquest camp és opcional. Fem servir Tavily com a proveïdor de motor de cerca per defecte." }, "SETTINGS$SEARCH_API_KEY_INSTRUCTIONS": { "en": "Get your API key from Tavily", @@ -2941,7 +3124,8 @@ "ar": "احصل على مفتاح API الخاص بك من Tavily", "fr": "Obtenez votre clé API de Tavily", "tr": "API anahtarınızı Tavily'den alın", - "uk": "Отримайте свій ключ API від Tavily" + "uk": "Отримайте свій ключ API від Tavily", + "ca": "Obteniu la vostra clau d'API a Tavily" }, "SETTINGS$CUSTOM_MODEL": { "en": "Custom Model", @@ -2957,7 +3141,8 @@ "ar": "نموذج مخصص", "fr": "Modèle personnalisé", "tr": "Özel Model", - "uk": "Користувацька модель" + "uk": "Користувацька модель", + "ca": "Model personalitzat" }, "GITHUB$CODE_NOT_IN_GITHUB": { "en": "Code not in GitHub?", @@ -2973,7 +3158,8 @@ "ar": "الكود غير موجود على GitHub؟", "fr": "Code non présent sur GitHub ?", "tr": "Kod GitHub'da değil mi?", - "uk": "Коду немає на GitHub?" + "uk": "Коду немає на GitHub?", + "ca": "El codi no és a GitHub?" }, "GITHUB$START_FROM_SCRATCH": { "en": "Start from scratch", @@ -2989,7 +3175,8 @@ "ar": "البدء من الصفر", "fr": "Commencer de zéro", "tr": "Sıfırdan başla", - "uk": "Почати з нуля" + "uk": "Почати з нуля", + "ca": "Comença des de zero" }, "AVATAR$ALT_TEXT": { "en": "user avatar", @@ -3005,7 +3192,8 @@ "ar": "صورة المستخدم", "fr": "Avatar utilisateur", "tr": "Kullanıcı avatarı", - "uk": "аватар користувача" + "uk": "аватар користувача", + "ca": "avatar d'usuari" }, "BRANDING$OPENHANDS": { "en": "OpenHands", @@ -3021,7 +3209,8 @@ "ar": "OpenHands", "fr": "OpenHands", "tr": "OpenHands", - "uk": "OpenHands" + "uk": "OpenHands", + "ca": "OpenHands" }, "BRANDING$OPENHANDS_LOGO": { "en": "OpenHands Logo", @@ -3037,7 +3226,8 @@ "ar": "شعار OpenHands", "fr": "Logo OpenHands", "tr": "OpenHands Logosu", - "uk": "OpenHands лого" + "uk": "OpenHands лого", + "ca": "Logotip d'OpenHands" }, "ERROR$GENERIC": { "en": "An error occurred", @@ -3053,7 +3243,8 @@ "ar": "حدث خطأ", "fr": "Une erreur s'est produite", "tr": "Bir hata oluştu", - "uk": "Сталася помилка" + "uk": "Сталася помилка", + "ca": "S'ha produït un error" }, "GITHUB$AUTH_SCOPE": { "en": "openid email profile", @@ -3069,7 +3260,8 @@ "ar": "openid email profile", "fr": "openid email profile", "tr": "openid email profile", - "uk": "openid email profile" + "uk": "openid email profile", + "ca": "openid email profile" }, "FILE_SERVICE$INVALID_FILE_PATH": { "en": "Invalid file path. Please check the file name and try again.", @@ -3085,7 +3277,8 @@ "fr": "Chemin de fichier invalide. Veuillez vérifier le nom du fichier et réessayer.", "tr": "Geçersiz dosya yolu. Lütfen dosya adını kontrol edin ve tekrar deneyin.", "ja": "ファイルパスが無効です。ファイル名を確認して、もう一度お試しください。", - "uk": "Недійсний шлях до файлу. Перевірте ім'я файлу та спробуйте ще раз." + "uk": "Недійсний шлях до файлу. Перевірте ім'я файлу та спробуйте ще раз.", + "ca": "Ruta de fitxer no vàlida. Comproveu el nom del fitxer i torneu-ho a intentar." }, "VSCODE$OPEN": { "en": "Open in VS Code", @@ -3101,7 +3294,8 @@ "ar": "فتح في VS Code", "fr": "Ouvrir dans VS Code", "tr": "VS Code'da aç", - "uk": "Відкрити у VS Code" + "uk": "Відкрити у VS Code", + "ca": "Obre a VS Code" }, "VSCODE$TITLE": { "en": "VS Code", @@ -3117,7 +3311,8 @@ "ar": "VS Code", "fr": "VS Code", "tr": "VS Code", - "uk": "VS Code" + "uk": "VS Code", + "ca": "VS Code" }, "VSCODE$LOADING": { "en": "Loading VS Code...", @@ -3133,7 +3328,8 @@ "ar": "جاري تحميل VS Code...", "fr": "Chargement de VS Code...", "tr": "VS Code yükleniyor...", - "uk": "Завантажую VS Code..." + "uk": "Завантажую VS Code...", + "ca": "Carregant VS Code..." }, "VSCODE$URL_NOT_AVAILABLE": { "en": "VS Code URL not available", @@ -3149,7 +3345,8 @@ "ar": "رابط VS Code غير متوفر", "fr": "URL VS Code non disponible", "tr": "VS Code URL'si mevcut değil", - "uk": "VS Code URL недоступний" + "uk": "VS Code URL недоступний", + "ca": "URL de VS Code no disponible" }, "VSCODE$FETCH_ERROR": { "en": "Failed to fetch VS Code URL", @@ -3165,7 +3362,8 @@ "ar": "فشل في جلب رابط VS Code", "fr": "Échec de la récupération de l'URL VS Code", "tr": "VS Code URL'si alınamadı", - "uk": "Не вдалося отримати VS Code URL" + "uk": "Не вдалося отримати VS Code URL", + "ca": "No s'ha pogut obtenir la URL de VS Code" }, "VSCODE$CROSS_ORIGIN_WARNING": { "en": "The code editor cannot be embedded due to browser security restrictions. Cross-origin cookies are being blocked.", @@ -3181,7 +3379,8 @@ "ar": "لا يمكن تضمين محرر التعليمات البرمجية بسبب قيود أمان المتصفح. يتم حظر ملفات تعريف الارتباط عبر المصدر.", "fr": "L'éditeur de code ne peut pas être intégré en raison des restrictions de sécurité du navigateur. Les cookies cross-origin sont bloqués.", "tr": "Tarayıcı güvenlik kısıtlamaları nedeniyle kod düzenleyici yerleştirilemiyor. Çapraz kaynaklı çerezler engelleniyor.", - "uk": "Редактор коду не може бути вбудований через обмеження безпеки браузера. Блокуються файли cookie з різних джерел." + "uk": "Редактор коду не може бути вбудований через обмеження безпеки браузера. Блокуються файли cookie з різних джерел.", + "ca": "L'editor de codi no es pot incrustar a causa de les restriccions de seguretat del navegador. Les galetes d'origen creuat estan bloquejades." }, "VSCODE$URL_PARSE_ERROR": { "en": "Error parsing URL", @@ -3197,7 +3396,8 @@ "ar": "خطأ في تحليل عنوان URL", "fr": "Erreur d'analyse de l'URL", "tr": "URL ayrıştırma hatası", - "uk": "Помилка аналізу URL" + "uk": "Помилка аналізу URL", + "ca": "Error en analitzar la URL" }, "VSCODE$OPEN_IN_NEW_TAB": { "en": "Open in New Tab", @@ -3213,7 +3413,8 @@ "ar": "فتح في علامة تبويب جديدة", "fr": "Ouvrir dans un nouvel onglet", "tr": "Yeni Sekmede Aç", - "uk": "Відкрити в новій вкладці" + "uk": "Відкрити в новій вкладці", + "ca": "Obre en una pestanya nova" }, "INCREASE_TEST_COVERAGE": { "en": "Increase test coverage", @@ -3229,7 +3430,8 @@ "ar": "زيادة تغطية الاختبارات", "fr": "Augmenter la couverture des tests", "tr": "Test kapsamını artır", - "uk": "Збільшення охоплення тестами" + "uk": "Збільшення охоплення тестами", + "ca": "Augmenta la cobertura de proves" }, "AUTO_MERGE_PRS": { "en": "Auto-merge PRs", @@ -3245,7 +3447,8 @@ "ar": "دمج طلبات السحب تلقائياً", "fr": "Fusionner automatiquement les PR", "tr": "PR'ları otomatik birleştir", - "uk": "Автоматично поєднувати PRs" + "uk": "Автоматично поєднувати PRs", + "ca": "Fusió automàtica de PRs" }, "FIX_README": { "en": "Fix README", @@ -3261,7 +3464,8 @@ "ar": "إصلاح README", "fr": "Corriger le README", "tr": "README'yi düzelt", - "uk": "Виправити README" + "uk": "Виправити README", + "ca": "Corregeix el README" }, "CLEAN_DEPENDENCIES": { "en": "Clean dependencies", @@ -3277,7 +3481,8 @@ "ar": "تنظيف التبعيات", "fr": "Nettoyer les dépendances", "tr": "Bağımlılıkları temizle", - "uk": "Почистити залкжності" + "uk": "Почистити залкжності", + "ca": "Neteja les dependències" }, "CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_LABEL": { "en": "OpenHands Workspace directory", @@ -3293,7 +3498,8 @@ "fr": "Répertoire de l'espace de travail OpenHands", "tr": "OpenHands çalışma alanı dizini", "ja": "OpenHands ワークスペースディレクトリ", - "uk": "OpenHands Каталог робочих просторів" + "uk": "OpenHands Каталог робочих просторів", + "ca": "Directori de l'espai de treball d'OpenHands" }, "LLM$PROVIDER": { "en": "LLM Provider", @@ -3309,7 +3515,8 @@ "fr": "Fournisseur LLM", "tr": "LLM Sağlayıcı", "de": "LLM-Anbieter", - "uk": "LLM Постачальник" + "uk": "LLM Постачальник", + "ca": "Proveïdor de LLM" }, "LLM$SELECT_PROVIDER_PLACEHOLDER": { "en": "Select a provider", @@ -3325,7 +3532,8 @@ "fr": "Sélectionner un fournisseur", "tr": "Bir sağlayıcı seçin", "de": "Anbieter auswählen", - "uk": "Виберіть постачальника" + "uk": "Виберіть постачальника", + "ca": "Selecciona un proveïdor" }, "API$KEY": { "en": "API Key", @@ -3341,7 +3549,8 @@ "fr": "Clé API", "tr": "API Anahtarı", "de": "API-Schlüssel", - "uk": "API ключ" + "uk": "API ключ", + "ca": "Clau d'API" }, "API$DONT_KNOW_KEY": { "en": "Don't know your API key?", @@ -3357,7 +3566,8 @@ "fr": "Vous ne connaissez pas votre clé API ?", "tr": "API anahtarınızı bilmiyor musunuz?", "de": "API-Schlüssel unbekannt?", - "uk": "Не знаєте свого API-ключа?" + "uk": "Не знаєте свого API-ключа?", + "ca": "No coneixeu la vostra clau d'API?" }, "BUTTON$SAVE": { "en": "Save", @@ -3373,7 +3583,8 @@ "fr": "Enregistrer", "tr": "Kaydet", "de": "Speichern", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "BUTTON$CLOSE": { "en": "Close", @@ -3389,7 +3600,8 @@ "fr": "Fermer", "tr": "Kapat", "de": "Schließen", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "MODAL$CONFIRM_RESET_TITLE": { "en": "Are you sure?", @@ -3405,7 +3617,8 @@ "fr": "Êtes-vous sûr ?", "tr": "Emin misiniz?", "de": "Sind Sie sicher?", - "uk": "Are you sure?" + "uk": "Are you sure?", + "ca": "Esteu segur?" }, "MODAL$CONFIRM_RESET_MESSAGE": { "en": "All information will be deleted", @@ -3421,7 +3634,8 @@ "fr": "Toutes les informations seront supprimées", "tr": "Tüm bilgiler silinecek", "de": "Möchten Sie alle Einstellungen zurücksetzen?", - "uk": "Всю інформацію буде видалено" + "uk": "Всю інформацію буде видалено", + "ca": "Tota la informació s'eliminarà" }, "MODAL$END_SESSION_TITLE": { "en": "End Session", @@ -3437,7 +3651,8 @@ "fr": "Terminer la session", "tr": "Oturumu sonlandır", "de": "Sitzung beenden", - "uk": "Закінчити сеанс" + "uk": "Закінчити сеанс", + "ca": "Finalitza la sessió" }, "MODAL$END_SESSION_MESSAGE": { "en": "Changing workspace settings will end the current session", @@ -3453,7 +3668,8 @@ "fr": "La modification des paramètres de l'espace de travail mettra fin à la session en cours", "tr": "Çalışma alanı ayarlarını değiştirmek mevcut oturumu sonlandıracak", "de": "Möchten Sie die aktuelle Sitzung beenden?", - "uk": "Зміна налаштувань робочого простору завершить поточний сеанс" + "uk": "Зміна налаштувань робочого простору завершить поточний сеанс", + "ca": "Canviar la configuració de l'espai de treball finalitzarà la sessió actual" }, "BUTTON$END_SESSION": { "en": "End Session", @@ -3469,7 +3685,8 @@ "fr": "Terminer la session", "tr": "Oturumu sonlandır", "de": "Sitzung beenden", - "uk": "Закінчити сеанс" + "uk": "Закінчити сеанс", + "ca": "Finalitza la sessió" }, "BUTTON$LAUNCH": { "en": "Launch", @@ -3485,7 +3702,8 @@ "fr": "Lancer", "tr": "Başlat", "de": "Starten", - "uk": "Запустити" + "uk": "Запустити", + "ca": "Llança" }, "BUTTON$CANCEL": { "en": "Cancel", @@ -3501,7 +3719,8 @@ "fr": "Annuler", "tr": "İptal", "de": "Abbrechen", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "BUTTON$ADD": { "en": "Add", @@ -3517,7 +3736,8 @@ "fr": "Ajouter", "tr": "Ekle", "de": "Hinzufügen", - "uk": "Додати" + "uk": "Додати", + "ca": "Afegeix" }, "EXIT_PROJECT$CONFIRM": { "en": "Exit Project", @@ -3533,7 +3753,8 @@ "fr": "Quitter le projet", "tr": "Projeden çık", "de": "Projekt verlassen", - "uk": "Exit Project" + "uk": "Exit Project", + "ca": "Surt del projecte" }, "EXIT_PROJECT$TITLE": { "en": "Are you sure you want to exit this project?", @@ -3549,7 +3770,8 @@ "fr": "Êtes-vous sûr de vouloir quitter ce projet ?", "tr": "Bu projeden çıkmak istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie dieses Projekt verlassen möchten?", - "uk": "Ви впевнені, що хочете вийти з цього проєкту?" + "uk": "Ви впевнені, що хочете вийти з цього проєкту?", + "ca": "Esteu segur que voleu sortir d'aquest projecte?" }, "LANGUAGE$LABEL": { "en": "Language", @@ -3565,7 +3787,8 @@ "fr": "Langue", "tr": "Dil", "de": "Sprache", - "uk": "Мова" + "uk": "Мова", + "ca": "Idioma" }, "GITHUB$TOKEN_LABEL": { "en": "GitHub Token", @@ -3581,7 +3804,8 @@ "fr": "Jeton GitHub", "tr": "GitHub Jetonu", "de": "GitHub-Token", - "uk": "GitHub Токен" + "uk": "GitHub Токен", + "ca": "Token de GitHub" }, "GITHUB$HOST_LABEL": { "en": "GitHub Host (optional)", @@ -3597,7 +3821,8 @@ "fr": "Hôte GitHub (optionnel)", "tr": "GitHub Sunucusu (isteğe bağlı)", "de": "GitHub-Host (optional)", - "uk": "Хост GitHub (необов'язково)" + "uk": "Хост GitHub (необов'язково)", + "ca": "Servidor de GitHub (opcional)" }, "GITHUB$TOKEN_OPTIONAL": { "en": "GitHub Token (Optional)", @@ -3613,7 +3838,8 @@ "fr": "Jeton GitHub (facultatif)", "tr": "GitHub Jetonu (İsteğe bağlı)", "de": "(Optional)", - "uk": "Токен GitHub (необов'язково)" + "uk": "Токен GitHub (необов'язково)", + "ca": "Token de GitHub (opcional)" }, "GITHUB$GET_TOKEN": { "en": "Get your token", @@ -3629,7 +3855,8 @@ "fr": "Obtenir votre jeton", "tr": "Jetonunuzu alın", "de": "Token abrufen", - "uk": "Отримайте свій токен" + "uk": "Отримайте свій токен", + "ca": "Obteniu el vostre token" }, "GITHUB$TOKEN_HELP_TEXT": { "en": "Get your <0>GitHub token or <1>click here for instructions", @@ -3645,7 +3872,8 @@ "fr": "Obtenez votre <0>jeton GitHub ou <1>cliquez ici pour les instructions", "tr": "<0>GitHub jetonu alın veya <1>talimatlar için buraya tıklayın", "de": "Holen Sie sich Ihren <0>GitHub-Token oder <1>klicken Sie hier für Anweisungen", - "uk": "Отримайте свій <0>токен GitHub або <1>натисніть тут, щоб отримати інструкції" + "uk": "Отримайте свій <0>токен GitHub або <1>натисніть тут, щоб отримати інструкції", + "ca": "Obteniu el vostre <0>token de GitHub o <1>feu clic aquí per obtenir instruccions" }, "GITHUB$TOKEN_LINK_TEXT": { "en": "GitHub token", @@ -3661,7 +3889,8 @@ "fr": "jeton GitHub", "tr": "GitHub jetonu", "de": "GitHub-Token", - "uk": "Токен GitHub" + "uk": "Токен GitHub", + "ca": "token de GitHub" }, "GITHUB$INSTRUCTIONS_LINK_TEXT": { "en": "click here for instructions", @@ -3677,7 +3906,8 @@ "fr": "cliquez ici pour les instructions", "tr": "talimatlar için buraya tıklayın", "de": "klicken Sie hier für Anweisungen", - "uk": "натисніть тут, щоб отримати інструкції" + "uk": "натисніть тут, щоб отримати інструкції", + "ca": "feu clic aquí per obtenir instruccions" }, "COMMON$HERE": { "en": "here", @@ -3693,7 +3923,8 @@ "fr": "ici", "tr": "buradan", "de": "Hier", - "uk": "тут" + "uk": "тут", + "ca": "aquí" }, "GITHUB$TOKEN_INVALID": { "en": "Invalid GitHub token", @@ -3709,7 +3940,8 @@ "fr": "Jeton GitHub invalide", "tr": "Geçersiz GitHub jetonu", "de": "GitHub-Token ungültig", - "uk": "Недійсний токен GitHub" + "uk": "Недійсний токен GitHub", + "ca": "Token de GitHub no vàlid" }, "BUTTON$DISCONNECT": { "en": "Disconnect", @@ -3725,7 +3957,8 @@ "fr": "Déconnecter", "tr": "Bağlantıyı kes", "de": "Verbindung trennen", - "uk": "Відключитися" + "uk": "Відключитися", + "ca": "Desconnecta" }, "GITHUB$CONFIGURE_REPOS": { "en": "Configure Github Repositories", @@ -3741,7 +3974,8 @@ "fr": "Configurer les dépôts GitHub", "tr": "GitHub depolarını yapılandır", "de": "GitHub-Repositories konfigurieren", - "uk": "Налаштування репозиторіїв Github" + "uk": "Налаштування репозиторіїв Github", + "ca": "Configura els repositoris de GitHub" }, "SLACK$INSTALL_APP": { "en": "Install OpenHands Slack App", @@ -3757,7 +3991,8 @@ "fr": "Installer l'application Slack OpenHands", "tr": "OpenHands Slack uygulamasını yükle", "de": "OpenHands Slack-App installieren", - "uk": "Встановити додаток OpenHands Slack" + "uk": "Встановити додаток OpenHands Slack", + "ca": "Instal·la l'aplicació Slack d'OpenHands" }, "COMMON$CLICK_FOR_INSTRUCTIONS": { "en": "Click here for instructions", @@ -3773,7 +4008,8 @@ "fr": "Cliquez ici pour les instructions", "tr": "Talimatlar için buraya tıklayın", "de": "Für Anweisungen klicken", - "uk": "Натисніть тут, щоб отримати інструкції" + "uk": "Натисніть тут, щоб отримати інструкції", + "ca": "Feu clic aquí per obtenir instruccions" }, "LLM$SELECT_MODEL_PLACEHOLDER": { "en": "Select a model", @@ -3789,7 +4025,8 @@ "fr": "Sélectionner un modèle", "tr": "Bir model seçin", "de": "Modell auswählen", - "uk": "Виберіть модель" + "uk": "Виберіть модель", + "ca": "Selecciona un model" }, "LLM$MODEL": { "en": "LLM Model", @@ -3805,7 +4042,8 @@ "fr": "Modèle LLM", "tr": "LLM Modeli", "de": "LLM-Modell", - "uk": "LLM модель" + "uk": "LLM модель", + "ca": "Model de LLM" }, "CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER": { "en": "Default: ./workspace", @@ -3821,7 +4059,8 @@ "es": "Predeterminado: ./workspace", "ja": "デフォルト: ./workspace", "tr": "Çalışma alanı dizinini girin", - "uk": "За замовчуванням: ./workspace" + "uk": "За замовчуванням: ./workspace", + "ca": "Per defecte: ./workspace" }, "CONFIGURATION$MODAL_TITLE": { "en": "Configuration", @@ -3837,7 +4076,8 @@ "fr": "Configuration", "tr": "Konfigürasyon", "ja": "設定", - "uk": "Конфігурація" + "uk": "Конфігурація", + "ca": "Configuració" }, "CONFIGURATION$MODEL_SELECT_LABEL": { "en": "Model", @@ -3853,7 +4093,8 @@ "fr": "Modèle", "tr": "Model", "ja": "モデル", - "uk": "Модель" + "uk": "Модель", + "ca": "Model" }, "CONFIGURATION$MODEL_SELECT_PLACEHOLDER": { "en": "Select a model", @@ -3869,7 +4110,8 @@ "fr": "Sélectionner un modèle", "tr": "Model Seç", "ja": "モデルを選択", - "uk": "Виберіть модель" + "uk": "Виберіть модель", + "ca": "Selecciona un model" }, "CONFIGURATION$AGENT_SELECT_LABEL": { "en": "Agent", @@ -3885,7 +4127,8 @@ "fr": "Agent", "tr": "Ajan", "ja": "エージェント", - "uk": "Агент" + "uk": "Агент", + "ca": "Agent" }, "CONFIGURATION$AGENT_SELECT_PLACEHOLDER": { "en": "Select an agent", @@ -3901,7 +4144,8 @@ "fr": "Sélectionner un agent", "tr": "Ajan Seç", "ja": "エージェントを選択", - "uk": "Виберіть агента" + "uk": "Виберіть агента", + "ca": "Selecciona un agent" }, "CONFIGURATION$LANGUAGE_SELECT_LABEL": { "en": "Language", @@ -3917,7 +4161,8 @@ "es": "Idioma", "ja": "言語", "tr": "Dil", - "uk": "Мова" + "uk": "Мова", + "ca": "Idioma" }, "CONFIGURATION$LANGUAGE_SELECT_PLACEHOLDER": { "en": "Select a language", @@ -3933,7 +4178,8 @@ "fr": "Sélectionner une langue", "tr": "Dil Seç", "ja": "言語を選択", - "uk": "Виберіть мову" + "uk": "Виберіть мову", + "ca": "Selecciona un idioma" }, "CONFIGURATION$SECURITY_SELECT_LABEL": { "en": "Security analyzer", @@ -3949,7 +4195,8 @@ "fr": "Analyseur de sécurité", "tr": "Güvenlik analizörü", "ja": "セキュリティアナライザー", - "uk": "Аналізатор безпеки" + "uk": "Аналізатор безпеки", + "ca": "Analitzador de seguretat" }, "CONFIGURATION$SECURITY_SELECT_PLACEHOLDER": { "en": "Select a security analyzer (optional)", @@ -3965,7 +4212,8 @@ "fr": "Sélectionnez un analyseur de sécurité (facultatif)", "tr": "Bir güvenlik analizörü seçin (isteğe bağlı)", "ja": "セキュリティアナライザーを選択(オプション)", - "uk": "Виберіть аналізатор безпеки (необов'язково)" + "uk": "Виберіть аналізатор безпеки (необов'язково)", + "ca": "Selecciona un analitzador de seguretat (opcional)" }, "CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": { "en": "Close", @@ -3981,7 +4229,8 @@ "fr": "Fermer", "tr": "Kapat", "ja": "閉じる", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "CONFIGURATION$MODAL_SAVE_BUTTON_LABEL": { "en": "Save", @@ -3997,7 +4246,8 @@ "es": "Guardar", "ja": "保存", "tr": "Kaydet", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "CONFIGURATION$MODAL_RESET_BUTTON_LABEL": { "en": "Reset to defaults", @@ -4013,7 +4263,8 @@ "fr": "Réinitialiser aux valeurs par défaut", "tr": "Varsayılanlara Sıfırla", "ja": "デフォルトにリセット", - "uk": "Скинути до налаштувань за замовчуванням" + "uk": "Скинути до налаштувань за замовчуванням", + "ca": "Restableix els valors per defecte" }, "STATUS$CONNECTED_TO_SERVER": { "en": "Connected to server", @@ -4029,7 +4280,8 @@ "ar": "متصل بالخادم", "fr": "Connecté au serveur", "tr": "Sunucuya bağlandı", - "uk": "Підключено до сервера" + "uk": "Підключено до сервера", + "ca": "Connectat al servidor" }, "PROJECT$NEW_PROJECT": { "en": "New Project", @@ -4045,7 +4297,8 @@ "ar": "مشروع جديد", "fr": "Nouveau projet", "tr": "Yeni Proje", - "uk": "Новий проект" + "uk": "Новий проект", + "ca": "Nou projecte" }, "BROWSER$SCREENSHOT": { "en": "Browser Screenshot", @@ -4061,7 +4314,8 @@ "it": "Screenshot del browser", "pt": "Captura de tela do navegador", "es": "Captura de pantalla del navegador", - "uk": "Знімок екрана браузера" + "uk": "Знімок екрана браузера", + "ca": "Captura de pantalla del navegador" }, "TIME$MINUTES_AGO": { "en": "minutes ago", @@ -4077,7 +4331,8 @@ "ar": "دقائق مضت", "fr": "minutes", "tr": "dakika önce", - "uk": "хвилин тому" + "uk": "хвилин тому", + "ca": "minuts enrere" }, "TIME$HOURS_AGO": { "en": "hours ago", @@ -4093,7 +4348,8 @@ "ar": "ساعات مضت", "fr": "heures", "tr": "saat önce", - "uk": "годин тому" + "uk": "годин тому", + "ca": "hores enrere" }, "TIME$DAYS_AGO": { "en": "days ago", @@ -4109,7 +4365,8 @@ "ar": "أيام مضت", "fr": "jours", "tr": "gün önce", - "uk": "днів тому" + "uk": "днів тому", + "ca": "dies enrere" }, "SETTINGS_FORM$RUNTIME_SIZE_LABEL": { "en": "Runtime Settings", @@ -4125,7 +4382,8 @@ "fr": "Paramètres d'exécution", "tr": "Çalışma Zamanı Ayarları", "ja": "ランタイムサイズ", - "uk": "Налаштування середовища виконання" + "uk": "Налаштування середовища виконання", + "ca": "Configuració de l'entorn d'execució" }, "CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE": { "en": "We've changed some settings in the latest update. Take a minute to review.", @@ -4141,7 +4399,8 @@ "fr": "Nous avons modifié certains paramètres dans la dernière mise à jour. Prenez un moment pour les examiner.", "tr": "Son güncellemede bazı ayarları değiştirdik. Gözden geçirmek için bir dakikanızı ayırın.", "ja": "最新のアップデートで一部の設定を変更しました。確認のためにお時間をいただけますか。", - "uk": "Ми змінили деякі налаштування в останньому оновленні. Приділіть хвилинку, щоб переглянути їх." + "uk": "Ми змінили деякі налаштування в останньому оновленні. Приділіть хвилинку, щоб переглянути їх.", + "ca": "Hem canviat alguns paràmetres en l'última actualització. Preneu un moment per revisar-los." }, "CONFIGURATION$AGENT_LOADING": { "en": "Please wait while the agent loads. This may take a few minutes...", @@ -4157,7 +4416,8 @@ "fr": "Veuillez patienter pendant le chargement de l'agent. Cela peut prendre quelques minutes...", "tr": "Lütfen ajan yüklenirken bekleyin. Bu birkaç dakika sürebilir...", "ja": "エージェントの読み込み中です。数分かかる場合があります...", - "uk": "Будь ласка, зачекайте, поки завантажиться агент. Це може тривати кілька хвилин...." + "uk": "Будь ласка, зачекайте, поки завантажиться агент. Це може тривати кілька хвилин....", + "ca": "Espereu mentre es carrega l'agent. Això pot trigar uns minuts..." }, "CONFIGURATION$AGENT_RUNNING": { "en": "Please stop the agent before editing these settings.", @@ -4173,7 +4433,8 @@ "fr": "Veuillez arrêter l'agent avant de modifier ces paramètres.", "tr": "Bu ayarları düzenlemeden önce lütfen ajanı durdurun.", "ja": "これらの設定を編集する前にエージェントを停止してください。", - "uk": "Будь ласка, зупиніть агента, перш ніж редагувати ці налаштування." + "uk": "Будь ласка, зупиніть агента, перш ніж редагувати ці налаштування.", + "ca": "Atureu l'agent abans d'editar aquests paràmetres." }, "CONFIGURATION$ERROR_FETCH_MODELS": { "en": "Failed to fetch models and agents", @@ -4189,7 +4450,8 @@ "tr": "Modeller ve ajanlar getirilemedi", "no": "Kunne ikke hente modeller og agenter", "ja": "モデルとエージェントの取得に失敗しました", - "uk": "Не вдалося отримати моделі та агентів" + "uk": "Не вдалося отримати моделі та агентів", + "ca": "No s'han pogut obtenir els models i agents" }, "CONFIGURATION$SETTINGS_NOT_FOUND": { "en": "Settings not found. Please check your API key", @@ -4205,7 +4467,8 @@ "fr": "Paramètres non trouvés. Veuillez vérifier votre clé API", "it": "Impostazioni non trovate. Controlla la tua chiave API", "pt": "Configurações não encontradas. Por favor, verifique sua chave API", - "tr": "Ayarlar bulunamadı. Lütfen API anahtarınızı kontrol edin" + "tr": "Ayarlar bulunamadı. Lütfen API anahtarınızı kontrol edin", + "ca": "Configuració no trobada. Comproveu la vostra clau d'API" }, "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE": { "en": "terms of service", @@ -4221,7 +4484,8 @@ "fr": "conditions d'utilisation", "it": "termini di servizio", "pt": "termos de serviço", - "tr": "hizmet şartları" + "tr": "hizmet şartları", + "ca": "condicions del servei" }, "SESSION$SERVER_CONNECTED_MESSAGE": { "en": "Connected to server", @@ -4237,7 +4501,8 @@ "tr": "Sunucuya bağlandı", "no": "Koblet til server", "uk": "Підключено до сервера", - "ja": "サーバーに接続しました" + "ja": "サーバーに接続しました", + "ca": "Connectat al servidor" }, "SESSION$SESSION_HANDLING_ERROR_MESSAGE": { "en": "Error handling message", @@ -4253,7 +4518,8 @@ "tr": "Mesaj işlenirken hata oluştu", "no": "Feil ved behandling av melding", "ja": "メッセージの処理中にエラーが発生しました", - "uk": "Помилка обробки повідомлення" + "uk": "Помилка обробки повідомлення", + "ca": "Error en gestionar el missatge" }, "SESSION$SESSION_CONNECTION_ERROR_MESSAGE": { "en": "Error connecting to session", @@ -4269,7 +4535,8 @@ "tr": "Oturuma bağlanırken hata oluştu", "no": "Feil ved tilkobling til økt", "ja": "セッションへの接続中にエラーが発生しました", - "uk": "Помилка підключення до сеансу" + "uk": "Помилка підключення до сеансу", + "ca": "Error en connectar a la sessió" }, "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE": { "en": "Socket not initialized", @@ -4285,7 +4552,8 @@ "tr": "Soket başlatılmadı", "no": "Socket ikke initialisert", "ja": "ソケットが初期化されていません", - "uk": "Сокет не ініціалізовано" + "uk": "Сокет не ініціалізовано", + "ca": "El socket no s'ha inicialitzat" }, "SESSION$TIMEOUT_MESSAGE": { "en": "Session Timeout!", @@ -4301,7 +4569,8 @@ "tr": "Oturum Zaman Aşımı!", "no": "Økten har tidsavbrutt!", "ja": "セッションタイムアウト!", - "uk": "Час сеансу минув!" + "uk": "Час сеансу минув!", + "ca": "La sessió ha caducat!" }, "EXPLORER$UPLOAD_ERROR_MESSAGE": { "en": "Error uploading file", @@ -4317,7 +4586,8 @@ "tr": "Dosya yüklenirken hata oluştu", "no": "Feil ved opplasting av fil", "ja": "ファイルのアップロード中にエラーが発生しました", - "uk": "Помилка завантаження файлу" + "uk": "Помилка завантаження файлу", + "ca": "Error en carregar el fitxer" }, "EXPLORER$LABEL_DROP_FILES": { "en": "Drop files here", @@ -4333,7 +4603,8 @@ "ar": "أسقط الملفات هنا", "tr": "Dosyaları buraya bırakın", "ja": "ここにファイルをドロップ", - "uk": "Перетягніть файли сюди" + "uk": "Перетягніть файли сюди", + "ca": "Deixeu els fitxers aquí" }, "EXPLORER$UPLOAD_SUCCESS_MESSAGE": { "en": "Successfully uploaded {{count}} file(s)", @@ -4349,7 +4620,8 @@ "tr": "{{count}} dosya başarıyla yüklendi", "no": "Lastet opp {{count}} fil(er) vellykket", "ja": "{{count}}個のファイルが正常にアップロードされました", - "uk": "Успішно завантажено файлів: {{count}}" + "uk": "Успішно завантажено файлів: {{count}}", + "ca": "S'han carregat {{count}} fitxer(s) correctament" }, "EXPLORER$NO_FILES_UPLOADED_MESSAGE": { "en": "No files were uploaded", @@ -4365,7 +4637,8 @@ "tr": "Hiçbir dosya yüklenmedi", "no": "Ingen filer ble lastet opp", "ja": "アップロードされたファイルはありません", - "uk": "Жодних файлів не завантажено" + "uk": "Жодних файлів не завантажено", + "ca": "No s'ha carregat cap fitxer" }, "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE": { "en": "{{count}} file(s) were skipped during upload", @@ -4381,7 +4654,8 @@ "tr": "Yükleme sırasında {{count}} dosya atlandı", "no": "{{count}} fil(er) ble hoppet over under opplasting", "ja": "アップロード中に{{count}}個のファイルがスキップされました", - "uk": "{{count}} файлів було пропущено під час завантаження" + "uk": "{{count}} файлів було пропущено під час завантаження", + "ca": "{{count}} fitxer(s) s'han omès durant la càrrega" }, "EXPLORER$UPLOAD_UNEXPECTED_RESPONSE_MESSAGE": { "en": "Unexpected response structure from server", @@ -4397,7 +4671,8 @@ "tr": "Sunucudan beklenmeyen yanıt yapısı", "no": "Uventet responsstruktur fra serveren", "ja": "サーバーから予期しない応答構造が返されました", - "uk": "Неочікувана структура відповіді від сервера" + "uk": "Неочікувана структура відповіді від сервера", + "ca": "Estructura de resposta inesperada del servidor" }, "EXPLORER$VSCODE_SWITCHING_MESSAGE": { "en": "Switching to VS Code in 3 seconds...\nImportant: Please inform the agent of any changes you make in VS Code. To avoid conflicts, wait for the assistant to complete its work before making your own changes.", @@ -4413,7 +4688,8 @@ "pt": "Mudando para o VS Code em 3 segundos...\nImportante: informe o agente sobre quaisquer alterações feitas no VS Code. Para evitar conflitos, aguarde até que o assistente conclua seu trabalho antes de fazer suas próprias alterações.", "es": "Cambiando a VS Code en 3 segundos...\nImportante: informe al agente de cualquier cambio realizado en VS Code. Para evitar conflictos, espere a que el asistente termine su trabajo antes de hacer sus propios cambios.", "tr": "3 saniye içinde VS Code'a geçiliyor...\nÖnemli: VS Code’da yaptığınız değişiklikleri aracıya bildirin. Çakışmaları önlemek için kendi değişikliklerinizi yapmadan önce asistanın işini bitirmesini bekleyin.", - "uk": "Перехід до VS Code через 3 секунди...\nВажливо: повідомте агента про будь-які зміни, які ви робите у VS Code. Щоб уникнути конфліктів, дочекайтеся завершення роботи помічника перед власними змінами." + "uk": "Перехід до VS Code через 3 секунди...\nВажливо: повідомте агента про будь-які зміни, які ви робите у VS Code. Щоб уникнути конфліктів, дочекайтеся завершення роботи помічника перед власними змінами.", + "ca": "Canviant a VS Code en 3 segons...\nImportant: Informeu l'agent de qualsevol canvi que feu a VS Code. Per evitar conflictes, espereu que l'assistent completi el seu treball abans de fer els vostres propis canvis." }, "EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE": { "en": "Error switching to VS Code: {{error}}", @@ -4429,7 +4705,8 @@ "pt": "Erro ao mudar para o VS Code: {{error}}", "es": "Error al cambiar a VS Code: {{error}}", "tr": "VS Code'a geçişte hata: {{error}}", - "uk": "Помилка перемикання на VS Code: {{error}}" + "uk": "Помилка перемикання на VS Code: {{error}}", + "ca": "Error en canviar a VS Code: {{error}}" }, "LOAD_SESSION$MODAL_TITLE": { "en": "Return to existing session?", @@ -4445,7 +4722,8 @@ "tr": "Mevcut oturuma dönmek ister misiniz?", "ja": "既存のセッションに戻りますか?", "no": "Gå tilbake til eksisterende økt?", - "uk": "Повернутися до існуючого сеансу?" + "uk": "Повернутися до існуючого сеансу?", + "ca": "Tornar a la sessió existent?" }, "LOAD_SESSION$MODAL_CONTENT": { "en": "You seem to have an ongoing session. Would you like to pick up where you left off, or start fresh?", @@ -4461,7 +4739,8 @@ "zh-TW": "您似乎有一個未完成的任務。您想從上次離開的地方繼續還是重新開始?", "ja": "進行中のセッションがあるようです。中断した箇所から再開しますか、それとも新しく始めますか?", "no": "Det ser ut som du har en pågående økt. Vil du fortsette der du slapp, eller starte på nytt?", - "uk": "Схоже, у вас триває сеанс. Ви хочете продовжити з того місця, де зупинилися, чи почати спочатку?" + "uk": "Схоже, у вас триває сеанс. Ви хочете продовжити з того місця, де зупинилися, чи почати спочатку?", + "ca": "Sembla que teniu una sessió en curs. Voleu reprendre-la o començar de nou?" }, "LOAD_SESSION$RESUME_SESSION_MODAL_ACTION_LABEL": { "en": "Resume Session", @@ -4477,7 +4756,8 @@ "ar": "استئناف الجلسة", "tr": "Oturumu Devam Ettir", "ja": "セッションを再開", - "uk": "Відновити сеанс" + "uk": "Відновити сеанс", + "ca": "Reprèn la sessió" }, "LOAD_SESSION$START_NEW_SESSION_MODAL_ACTION_LABEL": { "en": "Start New Session", @@ -4493,7 +4773,8 @@ "tr": "Yeni Oturum Başlat", "ja": "新しいセッションを開始", "no": "Start ny økt", - "uk": "Почати новий сеанс" + "uk": "Почати новий сеанс", + "ca": "Inicia una sessió nova" }, "FEEDBACK$MODAL_TITLE": { "en": "Share feedback", @@ -4509,7 +4790,8 @@ "tr": "Geri bildirim paylaş", "ja": "フィードバックを共有", "no": "Del tilbakemelding", - "uk": "Поділитися відгуком" + "uk": "Поділитися відгуком", + "ca": "Comparteix comentaris" }, "FEEDBACK$MODAL_CONTENT": { "en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data.", @@ -4525,7 +4807,8 @@ "ar": "لمساعدتنا على التحسين، نقوم بجمع التعليقات من تفاعلاتك لتحسين مطالباتنا. من خلال إرسال هذا النموذج، فإنك توافق على جمعنا لهذه البيانات.", "tr": "Kendimizi geliştirmemize yardımcı olmak için, etkileşimlerinizden geri bildirim toplayarak ipuçlarımızı iyileştiriyoruz. Bu formu göndererek, bu verileri toplamamıza izin vermiş olursunuz.", "ja": "サービス改善のため、プロンプトの改善に向けてユーザーの操作からフィードバックを収集しています。このフォームを送信することで、データ収集に同意したことになります。", - "uk": "Щоб покращити роботу, ми збираємо відгуки про вашу взаємодію з нами, щоб покращити наші промпти. Надсилаючи цю форму, ви погоджуєтеся на збір цих даних." + "uk": "Щоб покращити роботу, ми збираємо відгуки про вашу взаємодію з нами, щоб покращити наші промпти. Надсилаючи цю форму, ви погоджуєтеся на збір цих даних.", + "ca": "Per ajudar-nos a millorar, recopilem comentaris de les vostres interaccions per millorar els nostres missatges. En enviar aquest formulari, accepteu que recopilem aquestes dades." }, "FEEDBACK$EMAIL_LABEL": { "en": "Your email", @@ -4541,7 +4824,8 @@ "tr": "E-posta adresiniz", "ja": "メールアドレス", "no": "Din e-post", - "uk": "Ваша електронна адреса" + "uk": "Ваша електронна адреса", + "ca": "El vostre correu electrònic" }, "FEEDBACK$CONTRIBUTE_LABEL": { "en": "Contribute to public dataset", @@ -4557,7 +4841,8 @@ "ar": "المساهمة في مجموعة البيانات العامة", "tr": "Genel veri setine katkıda bulun", "ja": "公開データセットに貢献", - "uk": "Зробити внесок у публічний датасет" + "uk": "Зробити внесок у публічний датасет", + "ca": "Contribueix al conjunt de dades públic" }, "FEEDBACK$SHARE_LABEL": { "en": "Share", @@ -4573,7 +4858,8 @@ "tr": "Paylaş", "ja": "共有", "no": "Del", - "uk": "Поділитися" + "uk": "Поділитися", + "ca": "Comparteix" }, "FEEDBACK$CANCEL_LABEL": { "en": "Cancel", @@ -4589,7 +4875,8 @@ "ar": "إلغاء", "tr": "İptal", "ja": "キャンセル", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "FEEDBACK$EMAIL_PLACEHOLDER": { "en": "Enter your email address", @@ -4605,7 +4892,8 @@ "pt": "Digite seu endereço de e-mail", "tr": "E-posta adresinizi girin", "ja": "メールアドレスを入力してください", - "uk": "Введіть свою адресу електронної пошти" + "uk": "Введіть свою адресу електронної пошти", + "ca": "Introduïu la vostra adreça de correu electrònic" }, "FEEDBACK$PASSWORD_COPIED_MESSAGE": { "en": "Password copied to clipboard.", @@ -4621,7 +4909,8 @@ "pt": "Senha copiada para a área de transferência.", "tr": "Parola panoya kopyalandı.", "ja": "パスワードがクリップボードにコピーされました。", - "uk": "Пароль скопійовано в буфер обміну." + "uk": "Пароль скопійовано в буфер обміну.", + "ca": "Contrasenya copiada al porta-retalls." }, "FEEDBACK$GO_TO_FEEDBACK": { "en": "Go to shared feedback", @@ -4637,7 +4926,8 @@ "pt": "Ir para feedback compartilhado", "tr": "Paylaşılan geri bildirimlere git", "ja": "共有されたフィードバックへ移動", - "uk": "Перейти до відгуку" + "uk": "Перейти до відгуку", + "ca": "Vés als comentaris compartits" }, "FEEDBACK$PASSWORD": { "en": "Password:", @@ -4653,7 +4943,8 @@ "pt": "Senha:", "tr": "Parola:", "ja": "パスワード:", - "uk": "Пароль:" + "uk": "Пароль:", + "ca": "Contrasenya:" }, "FEEDBACK$INVALID_EMAIL_FORMAT": { "en": "Invalid email format", @@ -4669,7 +4960,8 @@ "pt": "Formato de e-mail inválido", "tr": "Geçersiz e-posta biçimi", "ja": "無効なメールアドレス形式", - "uk": "Недійсний формат електронної пошти" + "uk": "Недійсний формат електронної пошти", + "ca": "Format de correu electrònic no vàlid" }, "FEEDBACK$FAILED_TO_SHARE": { "en": "Failed to share, please contact the developers:", @@ -4685,7 +4977,8 @@ "pt": "Falha ao compartilhar, entre em contato com os desenvolvedores:", "tr": "Paylaşım başarısız, lütfen geliştiricilerle iletişime geçin:", "ja": "共有に失敗しました。開発者に連絡してください:", - "uk": "Не вдалося поділитися, зв’яжіться з розробниками:" + "uk": "Не вдалося поділитися, зв’яжіться з розробниками:", + "ca": "No s'ha pogut compartir; contacteu amb els desenvolupadors:" }, "FEEDBACK$COPY_LABEL": { "en": "Copy", @@ -4701,7 +4994,8 @@ "pt": "Copiar", "tr": "Kopyala", "ja": "コピー", - "uk": "Копіювати" + "uk": "Копіювати", + "ca": "Copia" }, "FEEDBACK$SHARING_SETTINGS_LABEL": { "en": "Sharing settings", @@ -4717,7 +5011,8 @@ "pt": "Configurações de compartilhamento", "tr": "Paylaşım ayarları", "ja": "共有設定", - "uk": "Налаштування доступу" + "uk": "Налаштування доступу", + "ca": "Configuració de compartició" }, "SECURITY$UNKNOWN_ANALYZER_LABEL": { "en": "Unknown security analyzer chosen", @@ -4733,7 +5028,8 @@ "pt": "Analisador de segurança desconhecido escolhido", "tr": "Bilinmeyen güvenlik analizörü seçildi", "ja": "不明なセキュリティアナライザーが選択されました", - "uk": "Вибрано невідомий аналізатор безпеки" + "uk": "Вибрано невідомий аналізатор безпеки", + "ca": "S'ha seleccionat un analitzador de seguretat desconegut" }, "INVARIANT$UPDATE_POLICY_LABEL": { "en": "Update Policy", @@ -4749,7 +5045,8 @@ "pt": "Atualizar política", "tr": "İlkeyi güncelle", "ja": "ポリシーを更新", - "uk": "Політика оновлення" + "uk": "Політика оновлення", + "ca": "Actualitza la política" }, "INVARIANT$UPDATE_SETTINGS_LABEL": { "en": "Update Settings", @@ -4765,7 +5062,8 @@ "pt": "Atualizar configurações", "tr": "Ayarları güncelle", "ja": "設定を更新", - "uk": "Оновити налаштування" + "uk": "Оновити налаштування", + "ca": "Actualitza la configuració" }, "INVARIANT$SETTINGS_LABEL": { "en": "Settings", @@ -4781,7 +5079,8 @@ "pt": "Configurações", "tr": "Ayarlar", "ja": "設定", - "uk": "Налаштування" + "uk": "Налаштування", + "ca": "Configuració" }, "INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL": { "en": "Ask for user confirmation on risk severity:", @@ -4797,7 +5096,8 @@ "pt": "Solicitar confirmação do usuário sobre a gravidade do risco:", "tr": "Risk şiddeti için kullanıcı onayı iste:", "ja": "リスクの重大度についてユーザーの確認を求める:", - "uk": "Запит підтвердження користувача щодо ступеня ризику:" + "uk": "Запит підтвердження користувача щодо ступеня ризику:", + "ca": "Demana confirmació a l'usuari sobre la gravetat del risc:" }, "INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL": { "en": "Don't ask for confirmation", @@ -4813,7 +5113,8 @@ "pt": "Não solicitar confirmação", "tr": "Onay isteme", "ja": "確認を求めない", - "uk": "Не запитувати підтвердження" + "uk": "Не запитувати підтвердження", + "ca": "No demanis confirmació" }, "INVARIANT$INVARIANT_ANALYZER_LABEL": { "en": "Invariant Analyzer", @@ -4829,7 +5130,8 @@ "pt": "Analisador de invariantes", "tr": "Değişmez Analizörü", "ja": "不変条件アナライザー", - "uk": "Інваріантний аналізатор" + "uk": "Інваріантний аналізатор", + "ca": "Analitzador Invariant" }, "INVARIANT$INVARIANT_ANALYZER_MESSAGE": { "en": "Invariant Analyzer continuously monitors your OpenHands agent for security issues.", @@ -4845,7 +5147,8 @@ "pt": "O analisador de invariantes monitora continuamente seu agente OpenHands em busca de problemas de segurança.", "tr": "Değişmez Analizörü, OpenHands ajanınızı güvenlik sorunları için sürekli olarak izler.", "ja": "不変条件アナライザーは、OpenHandsエージェントのセキュリティ問題を継続的に監視します。", - "uk": "Invariant Analyzer постійно контролює ваш агент OpenHands на наявність проблем безпеки." + "uk": "Invariant Analyzer постійно контролює ваш агент OpenHands на наявність проблем безпеки.", + "ca": "L'Analitzador Invariant monitoritza contínuament el vostre agent OpenHands per detectar problemes de seguretat." }, "INVARIANT$CLICK_TO_LEARN_MORE_LABEL": { "en": "Click to learn more", @@ -4861,7 +5164,8 @@ "pt": "Clique para saber mais", "tr": "Daha fazla bilgi için tıklayın", "ja": "詳細はこちらをクリック", - "uk": "Click to learn more" + "uk": "Click to learn more", + "ca": "Feu clic per saber-ne més" }, "INVARIANT$POLICY_LABEL": { "en": "Policy", @@ -4877,7 +5181,8 @@ "pt": "Política", "tr": "İlke", "ja": "ポリシー", - "uk": "Політика" + "uk": "Політика", + "ca": "Política" }, "INVARIANT$LOG_LABEL": { "en": "Logs", @@ -4893,7 +5198,8 @@ "pt": "Logs", "tr": "Günlükler", "ja": "ログ", - "uk": "Журнали" + "uk": "Журнали", + "ca": "Registres" }, "INVARIANT$EXPORT_TRACE_LABEL": { "en": "Export Trace", @@ -4909,7 +5215,8 @@ "pt": "Exportar rastreamento", "tr": "İzlemeyi dışa aktar", "ja": "トレースをエクスポート", - "uk": "Експорт трасування" + "uk": "Експорт трасування", + "ca": "Exporta el rastre" }, "INVARIANT$TRACE_EXPORTED_MESSAGE": { "en": "Trace exported", @@ -4925,7 +5232,8 @@ "pt": "Rastreamento exportado", "tr": "İzleme dışa aktarıldı", "ja": "トレースをエクスポートしました", - "uk": "Трасування експортовано" + "uk": "Трасування експортовано", + "ca": "Rastre exportat" }, "INVARIANT$POLICY_UPDATED_MESSAGE": { "en": "Policy updated", @@ -4941,7 +5249,8 @@ "pt": "Política atualizada", "tr": "İlke güncellendi", "ja": "ポリシーを更新しました", - "uk": "Policy updated" + "uk": "Policy updated", + "ca": "Política actualitzada" }, "INVARIANT$SETTINGS_UPDATED_MESSAGE": { "en": "Settings updated", @@ -4957,7 +5266,8 @@ "pt": "Configurações atualizadas", "tr": "Ayarlar güncellendi", "ja": "設定を更新しました", - "uk": "Налаштування оновлено" + "uk": "Налаштування оновлено", + "ca": "Configuració actualitzada" }, "CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE": { "en": "NEW FILES ADDED", @@ -4973,7 +5283,8 @@ "fr": "NOUVEAUX FICHIERS AJOUTÉS", "tr": "YENİ DOSYALAR EKLENDİ", "ja": "新しいファイルが追加されました", - "uk": "ДОДАНО НОВІ ФАЙЛИ" + "uk": "ДОДАНО НОВІ ФАЙЛИ", + "ca": "FITXERS NOUS AFEGITS" }, "CHAT_INTERFACE$DISCONNECTED": { "en": "Disconnected", @@ -4989,7 +5300,8 @@ "fr": "Déconnecté", "tr": "Bağlantı kesildi", "de": "Getrennt", - "uk": "Від'єднано" + "uk": "Від'єднано", + "ca": "Desconnectat" }, "CHAT_INTERFACE$COMMANDS": { "en": "Commands", @@ -5005,7 +5317,8 @@ "pt": "Comandos", "es": "Comandos", "tr": "Komutlar", - "uk": "Команди" + "uk": "Команди", + "ca": "Comandes" }, "CHAT_INTERFACE$CONNECTING": { "en": "Connecting... (this may take 1-2 minutes)", @@ -5021,7 +5334,8 @@ "fr": "Connexion en cours... (cela peut prendre 1-2 minutes)", "tr": "Bağlanıyor... (bu 1-2 dakika sürebilir)", "de": "Verbindung wird hergestellt... (dies kann 1-2 Minuten dauern)", - "uk": "З'єднання... (це може зайняти 1-2 хвилини)" + "uk": "З'єднання... (це може зайняти 1-2 хвилини)", + "ca": "Connectant... (això pot trigar 1-2 minuts)" }, "CHAT_INTERFACE$STOPPED": { "en": "Stopped", @@ -5037,7 +5351,8 @@ "fr": "Arrêté", "tr": "Durduruldu", "de": "Angehalten", - "uk": "Зупинено" + "uk": "Зупинено", + "ca": "Aturat" }, "CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": { "en": "Initializing agent...", @@ -5053,7 +5368,8 @@ "fr": "Initialisation de l'agent...", "tr": "Ajan başlatılıyor...", "ja": "エージェントを初期化中...", - "uk": "Ініціалізація агента..." + "uk": "Ініціалізація агента...", + "ca": "Inicialitzant l'agent..." }, "CHAT_INTERFACE$AGENT_INIT_MESSAGE": { "en": "Agent is initialized, waiting for task...", @@ -5069,7 +5385,8 @@ "fr": "L'agent est initialisé, en attente de tâche...", "tr": "Ajan başlatıldı, görev bekleniyor...", "ja": "エージェントの初期化が完了しました。タスクを待機中...", - "uk": "Агент ініціалізовано, очікує завдання..." + "uk": "Агент ініціалізовано, очікує завдання...", + "ca": "L'agent s'ha inicialitzat, esperant una tasca..." }, "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE": { "en": "Agent is running task", @@ -5085,7 +5402,8 @@ "fr": "L'agent exécute la tâche", "tr": "Ajan görevi yürütüyor", "ja": "エージェントがタスクを実行中", - "uk": "Агент виконує завдання" + "uk": "Агент виконує завдання", + "ca": "L'agent està executant la tasca" }, "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE": { "en": "Agent is awaiting user input...", @@ -5101,7 +5419,8 @@ "fr": "L'agent attend l'entrée de l'utilisateur...", "tr": "Ajan kullanıcı girdisini bekliyor...", "ja": "エージェントがユーザー入力を待機中...", - "uk": "Агент очікує на введення даних від користувача..." + "uk": "Агент очікує на введення даних від користувача...", + "ca": "L'agent espera la resposta de l'usuari..." }, "CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE": { "en": "Agent is Rate Limited. Retrying...", @@ -5117,7 +5436,8 @@ "fr": "L'agent est limité en fréquence. Nouvelle tentative...", "tr": "Ajan hız sınırına ulaştı. Yeniden deniyor...", "ja": "エージェントがレート制限中。再試行しています...", - "uk": "Агента обмежено кількістю запитів. Повторюємо спробу..." + "uk": "Агента обмежено кількістю запитів. Повторюємо спробу...", + "ca": "L'agent ha superat el límit de velocitat. Reintentant..." }, "CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE": { "en": "Agent is rate-limited. Stopped.", @@ -5133,7 +5453,8 @@ "fr": "L'agent est limité en fréquence. Arrêté.", "tr": "Ajan hız sınırına ulaştı. Durduruldu.", "ja": "エージェントがレート制限中。停止しました。", - "uk": "Агента обмежено кількістю запитів. Зупинено." + "uk": "Агента обмежено кількістю запитів. Зупинено.", + "ca": "L'agent ha superat el límit de velocitat. Aturat." }, "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": { "en": "Agent has paused.", @@ -5149,7 +5470,8 @@ "fr": "L'agent a mis en pause.", "tr": "Ajan duraklatıldı.", "ja": "エージェントが一時停止中", - "uk": "Агента призупинено." + "uk": "Агента призупинено.", + "ca": "L'agent ha fet una pausa." }, "LANDING$TITLE": { "en": "Let's start building!", @@ -5165,7 +5487,8 @@ "ar": "هيا نبدأ البناء!", "no": "La oss begynne å bygge!", "tr": "Hadi inşa etmeye başlayalım!", - "uk": "Почнімо будувати!" + "uk": "Почнімо будувати!", + "ca": "Comencem a construir!" }, "LANDING$SUBTITLE": { "en": "OpenHands makes it easy to build and maintain software using a simple prompt.", @@ -5181,7 +5504,8 @@ "ar": "يجعل OpenHands من السهل بناء وصيانة البرمجيات باستخدام موجه بسيط.", "no": "OpenHands gjør det enkelt å bygge og vedlikeholde programvare ved hjelp av en enkel prompt.", "tr": "Yapay zeka destekli yazılım geliştirme", - "uk": "OpenHands спрощує створення та підтримку програмного забезпечення за допомогою простого командного рядка." + "uk": "OpenHands спрощує створення та підтримку програмного забезпечення за допомогою простого командного рядка.", + "ca": "OpenHands facilita la construcció i el manteniment de programari amb un simple missatge." }, "LANDING$START_HELP": { "en": "Not sure how to start?", @@ -5197,7 +5521,8 @@ "ar": "غير متأكد من كيفية البدء؟", "no": "Usikker på hvordan du skal begynne?", "tr": "Başlamak için yardım", - "uk": "Не знаєте, як почати?" + "uk": "Не знаєте, як почати?", + "ca": "No esteu segur de com començar?" }, "LANDING$START_HELP_LINK": { "en": "Read this", @@ -5213,7 +5538,8 @@ "ar": "اقرأ هذا", "no": "Les dette", "tr": "Başlangıç kılavuzu", - "uk": "Прочитайте це" + "uk": "Прочитайте це", + "ca": "Llegiu això" }, "SUGGESTIONS$HELLO_WORLD": { "en": "Create a Hello World app", @@ -5229,7 +5555,8 @@ "ar": "إنشاء تطبيق Hello World", "no": "Lag en Hello World-app", "tr": "Bir Hello World uygulaması oluştur", - "uk": "Створіть додаток Hello World" + "uk": "Створіть додаток Hello World", + "ca": "Crea una aplicació Hola, món" }, "SUGGESTIONS$TODO_APP": { "en": "Build a todo list application", @@ -5245,7 +5572,8 @@ "ar": "بناء تطبيق قائمة المهام", "no": "Bygg en gjøremålsliste-app", "tr": "Yapılacaklar uygulaması", - "uk": "Створіть додаток для списку справ" + "uk": "Створіть додаток для списку справ", + "ca": "Construeix una aplicació de llista de tasques" }, "SUGGESTIONS$HACKER_NEWS": { "en": "Write a bash script that shows the top story on Hacker News", @@ -5261,7 +5589,8 @@ "ar": "كتابة سكربت باش يعرض أهم خبر على هاكر نيوز", "no": "Skriv et bash-script som viser topphistorien på Hacker News", "tr": "Hacker News klonu", - "uk": "Напишіть bash-скрипт, який показує головну новину на Hacker News" + "uk": "Напишіть bash-скрипт, який показує головну новину на Hacker News", + "ca": "Escriu un script bash que mostri la notícia principal de Hacker News" }, "LANDING$CHANGE_PROMPT": { "en": "What would you like to change in {{repo}}?", @@ -5277,7 +5606,8 @@ "ar": "ماذا تريد أن تغير في {{repo}}؟", "no": "Hva vil du endre i {{repo}}?", "tr": "İsteği değiştir", - "uk": "Що б ви хотіли змінити в {{repo}}?" + "uk": "Що б ви хотіли змінити в {{repo}}?", + "ca": "Què vols canviar a {{repo}}?" }, "GITHUB$CONNECT": { "en": "Connect to GitHub", @@ -5293,7 +5623,8 @@ "ar": "الاتصال بـ GitHub", "no": "Koble til GitHub", "tr": "GitHub'a bağlan", - "uk": "Підключитися до GitHub" + "uk": "Підключитися до GitHub", + "ca": "Connecta a GitHub" }, "GITHUB$NO_RESULTS": { "en": "No results found.", @@ -5309,7 +5640,8 @@ "uk": "Результатів не знайдено.", "no": "Ingen resultater funnet.", "ar": "لم يتم العثور على نتائج.", - "tr": "Sonuç bulunamadı." + "tr": "Sonuç bulunamadı.", + "ca": "No s'han trobat resultats." }, "GITHUB$LOADING_REPOSITORIES": { "en": "Loading repositories...", @@ -5325,7 +5657,8 @@ "ar": "لم يتم العثور على نتائج.", "no": "Ingen resultater funnet.", "tr": "Sonuç bulunamadı", - "uk": "Завантаження репозиторіїв..." + "uk": "Завантаження репозиторіїв...", + "ca": "Carregant repositoris..." }, "GITHUB$ADD_MORE_REPOS": { "en": "Add more repositories...", @@ -5341,7 +5674,8 @@ "ar": "إضافة المزيد من المستودعات...", "no": "Legg til flere repositories...", "tr": "Daha fazla depo ekle", - "uk": "Додати більше репозиторіїв..." + "uk": "Додати більше репозиторіїв...", + "ca": "Afegeix més repositoris..." }, "GITHUB$YOUR_REPOS": { "en": "Your Repos", @@ -5357,7 +5691,8 @@ "ar": "مستودعاتك", "no": "Dine repositories", "tr": "Depolarınız", - "uk": "Ваші репозиторії" + "uk": "Ваші репозиторії", + "ca": "Els teus repositoris" }, "GITHUB$PUBLIC_REPOS": { "en": "Public Repos", @@ -5373,7 +5708,8 @@ "ar": "المستودعات العامة", "no": "Offentlige repositories", "tr": "Herkese açık depolar", - "uk": "Публічні репозиторії" + "uk": "Публічні репозиторії", + "ca": "Repositoris públics" }, "DOWNLOAD$PREPARING": { "en": "Preparing Download...", @@ -5389,7 +5725,8 @@ "ar": "جاري تحضير التنزيل...", "no": "Forbereder nedlasting...", "tr": "Hazırlanıyor", - "uk": "Підготовка завантаження..." + "uk": "Підготовка завантаження...", + "ca": "Preparant la descàrrega..." }, "DOWNLOAD$DOWNLOADING": { "en": "Downloading Files", @@ -5405,7 +5742,8 @@ "ar": "جاري تنزيل الملفات", "no": "Laster ned filer", "tr": "İndiriliyor", - "uk": "Завантаження файлів" + "uk": "Завантаження файлів", + "ca": "Descarregant fitxers" }, "DOWNLOAD$FOUND_FILES": { "en": "Found {{count}} files...", @@ -5421,7 +5759,8 @@ "ar": "تم العثور على {{count}} ملف...", "no": "Fant {{count}} filer...", "tr": "Dosyalar bulundu", - "uk": "Знайдено {{count}} файлів..." + "uk": "Знайдено {{count}} файлів...", + "ca": "S'han trobat {{count}} fitxers..." }, "DOWNLOAD$SCANNING": { "en": "Scanning workspace...", @@ -5437,7 +5776,8 @@ "ar": "جاري فحص مساحة العمل...", "no": "Skanner arbeidsområde...", "tr": "Taranıyor", - "uk": "Сканування робочого простору..." + "uk": "Сканування робочого простору...", + "ca": "Analitzant l'espai de treball..." }, "DOWNLOAD$FILES_PROGRESS": { "en": "{{downloaded}} of {{total}} files", @@ -5453,7 +5793,8 @@ "ar": "{{downloaded}} من {{total}} ملف", "no": "{{downloaded}} av {{total}} filer", "tr": "Dosya ilerleme durumu", - "uk": "{{downloaded}} з {{total}} файлів" + "uk": "{{downloaded}} з {{total}} файлів", + "ca": "{{downloaded}} de {{total}} fitxers" }, "DOWNLOAD$CANCEL": { "en": "Cancel", @@ -5469,7 +5810,8 @@ "ar": "إلغاء", "no": "Avbryt", "tr": "İptal", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "ACTION$CONFIRM": { "en": "Confirm action", @@ -5485,7 +5827,8 @@ "ar": "تأكيد الإجراء", "no": "Bekreft handling", "tr": "Onayla", - "uk": "Підтвердити дію" + "uk": "Підтвердити дію", + "ca": "Confirma l'acció" }, "ACTION$REJECT": { "en": "Reject action", @@ -5501,7 +5844,8 @@ "ar": "رفض الإجراء", "no": "Avvis handling", "tr": "Reddet", - "uk": "Відхилити дію" + "uk": "Відхилити дію", + "ca": "Rebutja l'acció" }, "BADGE$BETA": { "en": "Beta", @@ -5517,7 +5861,8 @@ "ar": "تجريبي", "no": "Beta", "tr": "Beta", - "uk": "Бета" + "uk": "Бета", + "ca": "Beta" }, "AGENT$RESUME_TASK": { "en": "Resume the agent task", @@ -5533,7 +5878,8 @@ "ar": "استئناف مهمة الوكيل", "no": "Gjenoppta agentoppgaven", "tr": "Görevi devam ettir", - "uk": "Відновити завдання агента" + "uk": "Відновити завдання агента", + "ca": "Reprèn la tasca de l'agent" }, "AGENT$PAUSE_TASK": { "en": "Pause the current task", @@ -5549,7 +5895,8 @@ "ar": "إيقاف المهمة الحالية مؤقتًا", "no": "Pause gjeldende oppgave", "tr": "Görevi duraklat", - "uk": "Призупинити поточне завдання" + "uk": "Призупинити поточне завдання", + "ca": "Posa en pausa la tasca actual" }, "TOS$ACCEPT": { "en": "I accept the", @@ -5565,7 +5912,8 @@ "ar": "أوافق على", "no": "Jeg godtar", "tr": "Kabul et", - "uk": "Я приймаю" + "uk": "Я приймаю", + "ca": "Accepto les" }, "TOS$TERMS": { "en": "terms of service", @@ -5581,7 +5929,8 @@ "ar": "شروط الخدمة", "no": "vilkårene for bruk", "tr": "Kullanım şartları", - "uk": "умови обслуговування" + "uk": "умови обслуговування", + "ca": "condicions del servei" }, "USER$ACCOUNT_SETTINGS": { "en": "Account settings", @@ -5597,7 +5946,8 @@ "ar": "إعدادات الحساب", "no": "Kontoinnstillinger", "tr": "Hesap ayarları", - "uk": "Налаштування облікового запису" + "uk": "Налаштування облікового запису", + "ca": "Configuració del compte" }, "JUPYTER$OUTPUT_LABEL": { "en": "STDOUT/STDERR", @@ -5613,7 +5963,8 @@ "ar": "المخرجات القياسية/الأخطاء القياسية", "no": "STDOUT/STDERR", "tr": "Çıktı", - "uk": "STDOUT/STDERR" + "uk": "STDOUT/STDERR", + "ca": "STDOUT/STDERR" }, "BUTTON$STOP": { "en": "Stop", @@ -5629,7 +5980,8 @@ "ar": "توقف", "no": "Stopp", "tr": "Durdur", - "uk": "Стоп" + "uk": "Стоп", + "ca": "Atura" }, "BUTTON$PAUSE": { "en": "Pause", @@ -5645,7 +5997,8 @@ "ar": "إيقاف مؤقت", "no": "Pause", "tr": "Duraklat", - "uk": "Призупинити" + "uk": "Призупинити", + "ca": "Pausa" }, "BUTTON$EDIT_TITLE": { "en": "Edit Title", @@ -5661,7 +6014,8 @@ "ar": "تحرير العنوان", "no": "Rediger tittel", "tr": "Başlığı Düzenle", - "uk": "Редагувати заголовок" + "uk": "Редагувати заголовок", + "ca": "Edita el títol" }, "BUTTON$DOWNLOAD_VIA_VSCODE": { "en": "Download via VS Code", @@ -5677,7 +6031,8 @@ "ar": "تحميل عبر VS Code", "no": "Last ned via VS Code", "tr": "VS Code ile İndir", - "uk": "Завантажити через VS Code" + "uk": "Завантажити через VS Code", + "ca": "Descarrega via VS Code" }, "BUTTON$DISPLAY_COST": { "en": "Display Cost", @@ -5693,7 +6048,8 @@ "ar": "عرض التكلفة", "no": "Vis kostnad", "tr": "Maliyeti Göster", - "uk": "Показати вартість" + "uk": "Показати вартість", + "ca": "Mostra el cost" }, "BUTTON$SHOW_AGENT_TOOLS_AND_METADATA": { "en": "Show Agent Tools & Metadata", @@ -5709,7 +6065,8 @@ "ar": "عرض أدوات الوكيل والبيانات الوصفية", "no": "Vis agentverktøy og metadata", "tr": "Ajan Araçları ve Meta Verileri Göster", - "uk": "Показати інструменти агента та метадані" + "uk": "Показати інструменти агента та метадані", + "ca": "Mostra les eines i metadades de l'agent" }, "LANDING$ATTACH_IMAGES": { "en": "Attach images", @@ -5725,7 +6082,8 @@ "ar": "إرفاق صور", "no": "Legg ved bilder", "tr": "Resim ekle", - "uk": "Додати зображення" + "uk": "Додати зображення", + "ca": "Adjunta imatges" }, "LANDING$OPEN_REPO": { "en": "Open a Repo", @@ -5741,7 +6099,8 @@ "ar": "فتح مستودع", "no": "Åpne et repo", "tr": "Depo aç", - "uk": "Відкрити репозиторій" + "uk": "Відкрити репозиторій", + "ca": "Obre un repositori" }, "LANDING$REPLAY": { "en": "+ Replay Trajectory", @@ -5757,7 +6116,8 @@ "ar": "+ إعادة تشغيل المسار", "no": "+ Spill av trajektori", "tr": "+ Yörüngeyi yeniden oynat", - "uk": "+ Відтворити траєкторію" + "uk": "+ Відтворити траєкторію", + "ca": "+ Reprodueix la trajectòria" }, "LANDING$UPLOAD_TRAJECTORY": { "en": "Upload a .json", @@ -5773,7 +6133,8 @@ "no": "Last opp en .json-fil", "tr": ".json dosyası yükle", "uk": "Завантажити файл .json", - "ja": ".jsonファイルをアップロード" + "ja": ".jsonファイルをアップロード", + "ca": "Carrega un fitxer .json" }, "LANDING$RECENT_CONVERSATION": { "en": "jump back to your most recent conversation", @@ -5789,7 +6150,8 @@ "ar": "أو العودة إلى آخر محادثة", "no": "gå tilbake til din siste samtale", "tr": "Son konuşma", - "uk": "повернутися до вашої останньої розмови" + "uk": "повернутися до вашої останньої розмови", + "ca": "torna a la conversa més recent" }, "CONVERSATION$CONFIRM_DELETE": { "en": "Confirm Delete", @@ -5805,7 +6167,8 @@ "fr": "Confirmer la suppression", "tr": "Silmeyi Onayla", "de": "Löschen bestätigen", - "uk": "Підтвердити видалення" + "uk": "Підтвердити видалення", + "ca": "Confirma l'eliminació" }, "CONVERSATION$CONFIRM_STOP": { "en": "Confirm Stop", @@ -5821,7 +6184,8 @@ "fr": "Confirmer l'arrêt", "tr": "Durdurmayı Onayla", "de": "Stopp bestätigen", - "uk": "Підтвердити зупинку" + "uk": "Підтвердити зупинку", + "ca": "Confirma l'aturada" }, "CONVERSATION$CONFIRM_PAUSE": { "en": "Confirm Pause", @@ -5837,7 +6201,8 @@ "fr": "Confirmer la mise en pause", "tr": "Duraklatmayı Onayla", "de": "Pause bestätigen", - "uk": "Підтвердити призупинення" + "uk": "Підтвердити призупинення", + "ca": "Confirma la pausa" }, "CONVERSATION$PAUSE_WARNING": { "en": "Are you sure you want to pause this conversation?", @@ -5853,7 +6218,8 @@ "fr": "Êtes-vous sûr de vouloir mettre cette conversation en pause ?", "tr": "Bu konuşmayı duraklatmak istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie dieses Gespräch pausieren möchten?", - "uk": "Ви впевнені, що хочете призупинити цю розмову?" + "uk": "Ви впевнені, що хочете призупинити цю розмову?", + "ca": "Esteu segur que voleu posar en pausa aquesta conversa?" }, "CONVERSATION$CONFIRM_CLOSE_CONVERSATION": { "en": "Confirm Stop Sandbox", @@ -5869,7 +6235,8 @@ "fr": "Confirmer l'arrêt du sandbox", "tr": "Sandbox'ı Durdurmayı Onayla", "de": "Sandbox-Stopp bestätigen", - "uk": "Підтвердити зупинку пісочниці" + "uk": "Підтвердити зупинку пісочниці", + "ca": "Confirma l'aturada del sandbox" }, "CONVERSATION$CLOSE_CONVERSATION_WARNING": { "en": "This will stop the sandbox, and pause the following conversations:", @@ -5885,7 +6252,8 @@ "fr": "Cela arrêtera le sandbox et mettra en pause les conversations suivantes :", "tr": "Bu, sandbox'ı durduracak ve aşağıdaki konuşmaları duraklatacaktır:", "de": "Dies wird die Sandbox stoppen und die folgenden Gespräche pausieren:", - "uk": "Це зупинить пісочницю та призупинить наступні розмови:" + "uk": "Це зупинить пісочницю та призупинить наступні розмови:", + "ca": "Això aturarà el sandbox i posarà en pausa les converses següents:" }, "CONVERSATION$STOP_WARNING": { "en": "Are you sure you want to pause this conversation?", @@ -5901,7 +6269,8 @@ "fr": "Êtes-vous sûr de vouloir arrêter cette conversation ?", "tr": "Bu konuşmayı durdurmak istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie dieses Gespräch stoppen möchten?", - "uk": "Ви впевнені, що хочете зупинити цю розмову?" + "uk": "Ви впевнені, що хочете зупинити цю розмову?", + "ca": "Esteu segur que voleu posar en pausa aquesta conversa?" }, "CONVERSATION$METRICS_INFO": { "en": "Conversation Metrics", @@ -5917,7 +6286,8 @@ "fr": "Métriques de conversation", "tr": "Konuşma Metrikleri", "de": "Gesprächsmetriken", - "uk": "Метрики розмов" + "uk": "Метрики розмов", + "ca": "Mètriques de la conversa" }, "CONVERSATION$CREATED": { "en": "Created", @@ -5933,7 +6303,8 @@ "ar": "تم الإنشاء", "fr": "Créé", "tr": "Oluşturuldu", - "uk": "Створено" + "uk": "Створено", + "ca": "Creat" }, "CONVERSATION$AGO": { "en": "ago", @@ -5949,7 +6320,8 @@ "ar": "منذ", "fr": "il y a", "tr": "önce", - "uk": "тому" + "uk": "тому", + "ca": "fa" }, "GITHUB$VSCODE_LINK_DESCRIPTION": { "en": "and use the VS Code link to upload and download your code", @@ -5965,7 +6337,8 @@ "ar": "واستخدم رابط VS Code لتحميل وتنزيل الكود الخاص بك", "fr": "et utilisez le lien VS Code pour télécharger et téléverser votre code", "tr": "ve kodunuzu yüklemek ve indirmek için VS Code bağlantısını kullanın", - "uk": "і скористайтеся посиланням VS Code, щоб завантажити свій код" + "uk": "і скористайтеся посиланням VS Code, щоб завантажити свій код", + "ca": "i feu servir l'enllaç de VS Code per carregar i descarregar el vostre codi" }, "CONVERSATION$EXIT_WARNING": { "en": "Exit Conversation", @@ -5981,7 +6354,8 @@ "fr": "Quitter la conversation", "tr": "Konuşmadan Çık", "de": "Gespräch verlassen", - "uk": "Вийти з розмови" + "uk": "Вийти з розмови", + "ca": "Surt de la conversa" }, "CONVERSATION$TITLE_UPDATED": { "en": "Conversation title updated successfully", @@ -5997,7 +6371,8 @@ "ar": "تم تحديث عنوان المحادثة بنجاح", "fr": "Titre de la conversation mis à jour avec succès", "tr": "Konuşma başlığı başarıyla güncellendi", - "uk": "Назву розмови успішно оновлено" + "uk": "Назву розмови успішно оновлено", + "ca": "El títol de la conversa s'ha actualitzat correctament" }, "LANDING$OR": { "en": "Or", @@ -6013,7 +6388,8 @@ "ar": "أو", "no": "Eller", "tr": "veya", - "uk": "Або" + "uk": "Або", + "ca": "O" }, "SUGGESTIONS$TEST_COVERAGE": { "en": "Increase my test coverage", @@ -6029,7 +6405,8 @@ "ar": "زيادة تغطية الاختبار", "no": "Øk min testdekning", "tr": "Test kapsamı", - "uk": "Збільшити моє охоплення тестами" + "uk": "Збільшити моє охоплення тестами", + "ca": "Augmenta la meva cobertura de proves" }, "SUGGESTIONS$AUTO_MERGE": { "en": "Auto-merge Dependabot PRs", @@ -6045,7 +6422,8 @@ "ar": "دمج تلقائي لطلبات سحب Dependabot", "no": "Auto-flett Dependabot PRs", "tr": "Otomatik birleştirme", - "uk": "Автоматичне об'єднання Dependabot PR" + "uk": "Автоматичне об'єднання Dependabot PR", + "ca": "Fusió automàtica de les PR de Dependabot" }, "CHAT_INTERFACE$AGENT_STOPPED_MESSAGE": { "en": "Agent has stopped.", @@ -6061,7 +6439,8 @@ "fr": "L'agent s'est arrêté.", "tr": "Ajan durdu.", "ja": "エージェントが停止しました", - "uk": "Агент зупинився." + "uk": "Агент зупинився.", + "ca": "L'agent s'ha aturat." }, "CHAT_INTERFACE$AGENT_FINISHED_MESSAGE": { "en": "Agent has finished the task.", @@ -6077,7 +6456,8 @@ "fr": "L'agent a terminé la tâche.", "tr": "Ajan görevi tamamladı.", "ja": "エージェントがタスクを完了しました", - "uk": "Агент виконав завдання." + "uk": "Агент виконав завдання.", + "ca": "L'agent ha acabat la tasca." }, "CHAT_INTERFACE$AGENT_REJECTED_MESSAGE": { "en": "Agent has rejected the task.", @@ -6093,7 +6473,8 @@ "fr": "L'agent a rejeté la tâche.", "tr": "Ajan görevi reddetti.", "ja": "エージェントがタスクを拒否しました", - "uk": "Агент відхилив завдання." + "uk": "Агент відхилив завдання.", + "ca": "L'agent ha rebutjat la tasca." }, "CHAT_INTERFACE$AGENT_ERROR_MESSAGE": { "en": "Agent encountered an error.", @@ -6109,7 +6490,8 @@ "fr": "L'agent a rencontré une erreur.", "tr": "Ajan bir hatayla karşılaştı.", "ja": "エージェントでエラーが発生しました", - "uk": "Агент зіткнувся з помилкою." + "uk": "Агент зіткнувся з помилкою.", + "ca": "L'agent ha trobat un error." }, "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": { "en": "Agent is awaiting user confirmation for the pending action.", @@ -6125,7 +6507,8 @@ "fr": "L'agent attend la confirmation de l'utilisateur pour l'action en attente.", "tr": "Ajan, bekleyen işlem için kullanıcı onayını bekliyor.", "ja": "ユーザーの確認を待っています", - "uk": "Агент очікує підтвердження користувача для дії, що очікує на виконання." + "uk": "Агент очікує підтвердження користувача для дії, що очікує на виконання.", + "ca": "L'agent espera la confirmació de l'usuari per a l'acció pendent." }, "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": { "en": "Agent action has been confirmed!", @@ -6141,7 +6524,8 @@ "fr": "L'action de l'agent a été confirmée !", "tr": "Ajan eylemi onaylandı!", "ja": "ユーザーがアクションを承認しました", - "uk": "Дію агента підтверджено!" + "uk": "Дію агента підтверджено!", + "ca": "L'acció de l'agent ha estat confirmada!" }, "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": { "en": "Agent action has been rejected!", @@ -6157,7 +6541,8 @@ "fr": "L'action de l'agent a été rejetée !", "tr": "Ajan eylemi reddedildi!", "ja": "ユーザーがアクションを拒否しました", - "uk": "Дію агента відхилено!" + "uk": "Дію агента відхилено!", + "ca": "L'acció de l'agent ha estat rebutjada!" }, "CHAT_INTERFACE$INPUT_PLACEHOLDER": { "en": "Message assistant...", @@ -6173,7 +6558,8 @@ "fr": "Envoyez un message à l'assistant...", "tr": "Asistana mesaj gönder...", "ja": "メッセージを入力してください...", - "uk": "Написати помічнику..." + "uk": "Написати помічнику...", + "ca": "Envia un missatge a l'assistent..." }, "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE": { "en": "Continue", @@ -6189,7 +6575,8 @@ "fr": "Continuer", "tr": "Devam et", "ja": "続行するにはEnterを押してください", - "uk": "Продовжити" + "uk": "Продовжити", + "ca": "Continua" }, "CHAT_INTERFACE$USER_ASK_CONFIRMATION": { "en": "Do you want to continue with this action?", @@ -6205,7 +6592,8 @@ "fr": "Voulez-vous continuer avec cette action ?", "tr": "Bu işleme devam etmek istiyor musunuz?", "ja": "このアクションを実行してもよろしいですか?", - "uk": "Ви хочете продовжити цю дію?" + "uk": "Ви хочете продовжити цю дію?", + "ca": "Voleu continuar amb aquesta acció?" }, "CHAT_INTERFACE$HIGH_RISK_WARNING": { "en": "Review carefully before proceeding.", @@ -6221,7 +6609,8 @@ "fr": "Examinez attentivement avant de continuer.", "tr": "Devam etmeden önce dikkatlice gözden geçirin.", "ja": "続行する前に慎重に確認してください。", - "uk": "Уважно перевірте перед продовженням." + "uk": "Уважно перевірте перед продовженням.", + "ca": "Reviseu-ho amb cura abans de continuar." }, "CHAT_INTERFACE$USER_CONFIRMED": { "en": "Confirm the requested action", @@ -6237,7 +6626,8 @@ "fr": "Confirmer l'action demandée", "tr": "İstenen eylemi onayla", "ja": "ユーザーが承認しました", - "uk": "Підтвердьте запитувану дію" + "uk": "Підтвердьте запитувану дію", + "ca": "Confirma l'acció sol·licitada" }, "CHAT_INTERFACE$USER_REJECTED": { "en": "Reject the requested action", @@ -6253,7 +6643,8 @@ "fr": "Rejeter l'action demandée", "tr": "İstenen eylemi reddet", "ja": "ユーザーが拒否しました", - "uk": "Відхилити запитувану дію" + "uk": "Відхилити запитувану дію", + "ca": "Rebutja l'acció sol·licitada" }, "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": { "en": "Send", @@ -6269,7 +6660,8 @@ "fr": "Envoyer", "ja": "送信", "tr": "Gönder", - "uk": "Надіслати" + "uk": "Надіслати", + "ca": "Envia" }, "CHAT_INTERFACE$CHAT_MESSAGE_COPIED": { "en": "Message copied to clipboard", @@ -6285,7 +6677,8 @@ "fr": "Message copié dans le presse-papiers", "tr": "Mesaj panoya kopyalandı", "ja": "メッセージをコピーしました", - "uk": "Повідомлення скопійовано в буфер обміну" + "uk": "Повідомлення скопійовано в буфер обміну", + "ca": "Missatge copiat al porta-retalls" }, "CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED": { "en": "Failed to copy message to clipboard", @@ -6301,7 +6694,8 @@ "fr": "Échec de la copie du message dans le presse-papiers", "tr": "Mesaj panoya kopyalanamadı", "ja": "メッセージのコピーに失敗しました", - "uk": "Не вдалося скопіювати повідомлення в буфер обміну" + "uk": "Не вдалося скопіювати повідомлення в буфер обміну", + "ca": "No s'ha pogut copiar el missatge al porta-retalls" }, "CHAT_INTERFACE$TOOLTIP_COPY_MESSAGE": { "en": "Copy message", @@ -6317,7 +6711,8 @@ "fr": "Copier le message", "tr": "Mesajı kopyala", "ja": "メッセージをコピー", - "uk": "Копіювати повідомлення" + "uk": "Копіювати повідомлення", + "ca": "Copia el missatge" }, "CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE": { "en": "Send message", @@ -6333,7 +6728,8 @@ "fr": "Envoyer le message", "tr": "Mesaj gönder", "ja": "メッセージを送信", - "uk": "Надіслати повідомлення" + "uk": "Надіслати повідомлення", + "ca": "Envia el missatge" }, "CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE": { "en": "Upload image", @@ -6349,7 +6745,8 @@ "fr": "Télécharger une image", "tr": "Resim yükle", "ja": "画像をアップロード", - "uk": "Завантажити зображення" + "uk": "Завантажити зображення", + "ca": "Carrega una imatge" }, "CHAT_INTERFACE$INITIAL_MESSAGE": { "en": "Hi! I'm OpenHands, an AI Software Engineer. What would you like to build with me today?", @@ -6365,7 +6762,8 @@ "fr": "Salut! Je suis OpenHands, un ingénieur logiciel en IA. Que voudriez-vous construire avec moi aujourd'hui?", "ja": "何を作りたいですか?", "tr": "Ne yapmak istersiniz?", - "uk": "Привіт! Я OpenHands, ШІ інженер-програміст. Що б ви хотіли створити зі мною сьогодні?" + "uk": "Привіт! Я OpenHands, ШІ інженер-програміст. Що б ви хотіли створити зі мною сьогодні?", + "ca": "Hola! Soc l'OpenHands, un enginyer de programari d'IA. Què vols construir avui?" }, "CHAT_INTERFACE$ASSISTANT": { "en": "Assistant", @@ -6381,7 +6779,8 @@ "fr": "Assistant", "tr": "Gönder", "ja": "アシスタント", - "uk": "Помічник" + "uk": "Помічник", + "ca": "Assistent" }, "CHAT_INTERFACE$TO_BOTTOM": { "en": "To Bottom", @@ -6397,7 +6796,8 @@ "fr": "Vers le bas", "tr": "En alta", "ja": "一番下へ", - "uk": "Вниз" + "uk": "Вниз", + "ca": "Ves al final" }, "CHAT_INTERFACE$MESSAGE_ARIA_LABEL": { "en": "Message from {{sender}}", @@ -6413,7 +6813,8 @@ "fr": "Message de {{sender}}", "tr": "{{sender}} tarafından gönderilen mesaj", "ja": "メッセージ", - "uk": "Повідомлення від {{sender}}" + "uk": "Повідомлення від {{sender}}", + "ca": "Missatge de {{sender}}" }, "CHAT_INTERFACE$CHAT_CONVERSATION": { "en": "Chat Conversation", @@ -6429,7 +6830,8 @@ "fr": "Conversation de chat", "tr": "Sohbet Konuşması", "ja": "チャット会話", - "uk": "Розмова в чаті" + "uk": "Розмова в чаті", + "ca": "Conversa de xat" }, "CHAT_INTERFACE$UNKNOWN_SENDER": { "en": "Unknown", @@ -6445,7 +6847,8 @@ "fr": "Inconnu", "tr": "Bilinmeyen", "ja": "不明な送信者", - "uk": "Невідомий" + "uk": "Невідомий", + "ca": "Desconegut" }, "SETTINGS$MODEL_TOOLTIP": { "en": "Select the language model to use.", @@ -6461,7 +6864,8 @@ "fr": "Sélectionnez le modèle de langage à utiliser.", "tr": "Kullanılacak dil modelini seçin.", "ja": "使用するモデルを選択", - "uk": "Виберіть мовну модель, яку потрібно використовувати." + "uk": "Виберіть мовну модель, яку потрібно використовувати.", + "ca": "Selecciona el model de llenguatge que vols fer servir." }, "SETTINGS$AGENT_TOOLTIP": { "en": "Select the agent to use.", @@ -6477,7 +6881,8 @@ "fr": "Sélectionnez l'agent à utiliser.", "tr": "Kullanılacak ajanı seçin.", "ja": "使用するエージェントを選択", - "uk": "Виберіть агента для використання." + "uk": "Виберіть агента для використання.", + "ca": "Selecciona l'agent que vols fer servir." }, "SETTINGS$LANGUAGE_TOOLTIP": { "en": "Select the language for the UI.", @@ -6493,7 +6898,8 @@ "fr": "Sélectionnez la langue de l'interface utilisateur.", "tr": "Kullanıcı arayüzü için dil seçin.", "ja": "インターフェースの言語を選択", - "uk": "Виберіть мову для інтерфейсу користувача." + "uk": "Виберіть мову для інтерфейсу користувача.", + "ca": "Selecciona l'idioma de la interfície." }, "SETTINGS$DISABLED_RUNNING": { "en": "Cannot be changed while the agent is running.", @@ -6509,7 +6915,8 @@ "fr": "Ne peut pas être modifié pendant que l'agent est en cours d'exécution.", "tr": "Ajan çalışırken değiştirilemez.", "ja": "エージェントの実行中は設定を変更できません", - "uk": "Не можна змінити, поки агент працює." + "uk": "Не можна змінити, поки агент працює.", + "ca": "No es pot canviar mentre l'agent s'executa." }, "SETTINGS$API_KEY_PLACEHOLDER": { "en": "Enter your API key.", @@ -6525,7 +6932,8 @@ "fr": "Entrez votre clé API.", "tr": "API anahtarınızı girin.", "ja": "APIキーを入力", - "uk": "Введіть свій ключ API." + "uk": "Введіть свій ключ API.", + "ca": "Introduïu la vostra clau d'API." }, "SETTINGS$LLM_API_KEY": { "en": "OpenHands LLM Key", @@ -6541,7 +6949,8 @@ "fr": "Clé LLM OpenHands", "tr": "OpenHands LLM Anahtarı", "ja": "OpenHands LLMキー", - "uk": "Ключ LLM OpenHands" + "uk": "Ключ LLM OpenHands", + "ca": "Clau LLM d'OpenHands" }, "SETTINGS$LLM_API_KEY_DESCRIPTION": { "en": "You can use this API Key as the LLM API Key for OpenHands open-source and CLI. It will incur cost on your OpenHands Cloud account. Do NOT share this key elsewhere.", @@ -6557,7 +6966,8 @@ "fr": "Vous pouvez utiliser cette clé API comme clé API LLM pour OpenHands open-source et CLI. Cela entraînera des coûts sur votre compte OpenHands Cloud. NE partagez PAS cette clé ailleurs.", "tr": "Bu API Anahtarını, OpenHands açık kaynak ve CLI için LLM API Anahtarı olarak kullanabilirsiniz. OpenHands Cloud hesabınızda maliyet oluşturacaktır. Bu anahtarı başka yerlerde paylaşmayın.", "ja": "このAPIキーをOpenHandsオープンソースおよびCLIのLLM APIキーとして使用できます。OpenHands Cloudアカウントに費用が発生します。このキーを他の場所で共有しないでください。", - "uk": "Ви можете використовувати цей ключ API як ключ API LLM для OpenHands з відкритим кодом та CLI. Це призведе до витрат на вашому обліковому записі OpenHands Cloud. НЕ діліться цим ключем деінде." + "uk": "Ви можете використовувати цей ключ API як ключ API LLM для OpenHands з відкритим кодом та CLI. Це призведе до витрат на вашому обліковому записі OpenHands Cloud. НЕ діліться цим ключем деінде.", + "ca": "Podeu fer servir aquesta clau d'API com a clau d'API de LLM per al codi obert i la CLI d'OpenHands. Generarà costos al vostre compte d'OpenHands Cloud. NO compartiu aquesta clau en cap altre lloc." }, "SETTINGS$REFRESH_LLM_API_KEY": { "en": "Refresh API Key", @@ -6573,7 +6983,8 @@ "fr": "Actualiser la clé API", "tr": "API Anahtarını Yenile", "ja": "APIキーを更新", - "uk": "Оновити ключ API" + "uk": "Оновити ключ API", + "ca": "Actualitza la clau d'API" }, "SETTINGS$LLM_API_KEY_PAYWALL_MESSAGE": { "en": "Purchase at least $10 in credits to get access to OpenHands LLM key for use with OpenHands CLI and SDK.", @@ -6589,7 +7000,8 @@ "fr": "Achetez au moins 10$ de crédits pour accéder à la clé LLM OpenHands à utiliser avec OpenHands CLI et SDK.", "tr": "OpenHands CLI ve SDK ile kullanmak için OpenHands LLM Anahtarına erişim sağlamak için en az 10$ kredi satın alın.", "ja": "OpenHands CLIおよびSDKで使用するOpenHands LLMキーにアクセスするには、少なくとも$10のクレジットを購入してください。", - "uk": "Придбайте кредитів мінімум на $10, щоб отримати доступ до ключа LLM OpenHands для використання з OpenHands CLI та SDK." + "uk": "Придбайте кредитів мінімум на $10, щоб отримати доступ до ключа LLM OpenHands для використання з OpenHands CLI та SDK.", + "ca": "Compreu almenys 10 $ en crèdits per accedir a la clau LLM d'OpenHands per a la CLI i l'SDK d'OpenHands." }, "SETTINGS$LLM_API_KEY_BUY_NOW": { "en": "Buy Now", @@ -6605,7 +7017,8 @@ "fr": "Acheter maintenant", "tr": "Şimdi Satın Al", "ja": "今すぐ購入", - "uk": "Купити зараз" + "uk": "Купити зараз", + "ca": "Compra ara" }, "SETTINGS$CONFIRMATION_MODE": { "en": "Enable Confirmation Mode", @@ -6621,7 +7034,8 @@ "fr": "Activer le mode de confirmation", "tr": "Onay Modunu Etkinleştir", "ja": "確認モード", - "uk": "Увімкнути режим підтвердження" + "uk": "Увімкнути режим підтвердження", + "ca": "Activa el mode de confirmació" }, "SETTINGS$CONFIRMATION_MODE_TOOLTIP": { "en": "Awaits for user confirmation before executing code.", @@ -6637,7 +7051,8 @@ "fr": "Attend la confirmation de l'utilisateur avant d'exécuter le code.", "tr": "Kodu çalıştırmadan önce kullanıcı onayını bekler.", "ja": "エージェントのアクションを実行前に確認", - "uk": "Очікує підтвердження користувача перед виконанням коду." + "uk": "Очікує підтвердження користувача перед виконанням коду.", + "ca": "Espera la confirmació de l'usuari abans d'executar el codi." }, "SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP": { "en": "The agent is in confirmation mode. It will prompt the user to confirm certain actions when security analyzer policy detected a high-risk action. Click this icon to go to settings tab for more information.", @@ -6653,7 +7068,8 @@ "fr": "L'agent est en mode de confirmation. Il demandera à l'utilisateur de confirmer certaines actions lorsque la politique de l'analyseur de sécurité détecte une action à haut risque. Consultez l'onglet paramètres pour plus d'informations.", "tr": "Ajan onay modunda. Güvenlik analizörü politikası yüksek riskli bir eylem tespit ettiğinde kullanıcıdan belirli eylemleri onaylamasını isteyecek. Daha fazla bilgi için ayarlar sekmesini kontrol edin.", "ja": "エージェントは確認モードです。セキュリティアナライザーポリシーが高リスクアクションを検出した場合、特定のアクションの確認をユーザーに求めます。詳細については設定タブを確認してください。", - "uk": "Агент знаходиться в режимі підтвердження. Він попросить користувача підтвердити певні дії, коли політика аналізатора безпеки виявить дію високого ризику. Перевірте вкладку налаштувань для отримання додаткової інформації." + "uk": "Агент знаходиться в режимі підтвердження. Він попросить користувача підтвердити певні дії, коли політика аналізатора безпеки виявить дію високого ризику. Перевірте вкладку налаштувань для отримання додаткової інформації.", + "ca": "L'agent es troba en mode de confirmació. Demanarà confirmació a l'usuari per a determinades accions quan l'analitzador de política de seguretat detecti una acció d'alt risc. Feu clic en aquesta icona per anar a la pestanya de configuració per obtenir més informació." }, "SETTINGS$AGENT_SELECT_ENABLED": { "en": "Enable Agent Selection - Advanced Users", @@ -6669,7 +7085,8 @@ "fr": "Activer la sélection d'agent - Utilisateurs avancés", "tr": "Ajan Seçimini Etkinleştir - İleri Düzey Kullanıcılar", "ja": "エージェント選択を有効化", - "uk": "Увімкнути вибір агента – Досвідчені користувачі" + "uk": "Увімкнути вибір агента – Досвідчені користувачі", + "ca": "Activa la selecció d'agent - Usuaris avançats" }, "SETTINGS$SECURITY_ANALYZER": { "en": "Enable Security Analyzer", @@ -6685,7 +7102,8 @@ "fr": "Activer l'analyseur de sécurité", "tr": "Güvenlik Analizörünü Etkinleştir", "ja": "セキュリティアナライザー", - "uk": "Увімкнути аналізатор безпеки" + "uk": "Увімкнути аналізатор безпеки", + "ca": "Activa l'analitzador de seguretat" }, "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER": { "en": "Select a security analyzer…", @@ -6701,7 +7119,8 @@ "fr": "Sélectionnez un analyseur de sécurité…", "tr": "Bir güvenlik analizörü seçin…", "ja": "セキュリティアナライザーを選択…", - "uk": "Виберіть аналізатор безпеки…" + "uk": "Виберіть аналізатор безпеки…", + "ca": "Selecciona un analitzador de seguretat..." }, "SETTINGS$SECURITY_ANALYZER_TOOLTIP": { "en": "When enabled, the agent will pause and ask for confirmation when it tries to execute high-risk actions", @@ -6717,7 +7136,8 @@ "fr": "Lorsqu'il est activé, l'agent se mettra en pause et demandera confirmation lorsqu'il tentera d'exécuter des actions à haut risque", "tr": "Etkinleştirildiğinde, ajan yüksek riskli eylemleri gerçekleştirmeye çalıştığında duraklar ve onay ister", "ja": "有効にすると、エージェントは高リスクなアクションを実行しようとする際に一時停止し、確認を求めます", - "uk": "Коли увімкнено, агент зупиниться і попросить підтвердження, коли спробує виконати дії високого ризику" + "uk": "Коли увімкнено, агент зупиниться і попросить підтвердження, коли спробує виконати дії високого ризику", + "ca": "Quan estigui activat, l'agent farà una pausa i demanarà confirmació quan intenti executar accions d'alt risc" }, "SETTINGS$SECURITY_ANALYZER_DESCRIPTION": { "en": "The security analyzer will be used in conjunction with confirmation mode. By default, it utilizes LLM-predicted action risk to determine whether to prompt the user for confirmation. If the risk is HIGH, it will prompt the user for confirmation by default.", @@ -6733,7 +7153,8 @@ "fr": "L'analyseur de sécurité sera utilisé en conjonction avec le mode de confirmation. Par défaut, il utilise le risque d'action prédit par LLM pour déterminer s'il faut demander confirmation à l'utilisateur. Si le risque est ÉLEVÉ, il demandera confirmation par défaut.", "tr": "Güvenlik analizörü onay modu ile birlikte kullanılacaktır. Varsayılan olarak, kullanıcıdan onay istenip istenmeyeceğini belirlemek için LLM tarafından tahmin edilen eylem riskini kullanır. Risk YÜKSEK ise, varsayılan olarak kullanıcıdan onay isteyecektir.", "ja": "セキュリティアナライザーは確認モードと組み合わせて使用されます。デフォルトでは、LLMが予測したアクションリスクを利用して、ユーザーに確認を求めるかどうかを決定します。リスクが高い場合、デフォルトでユーザーに確認を求めます。", - "uk": "Аналізатор безпеки буде використовуватися разом з режимом підтвердження. За замовчуванням він використовує передбачений LLM ризик дії для визначення, чи потрібно запитувати підтвердження у користувача. Якщо ризик ВИСОКИЙ, він запитуватиме підтвердження за замовчуванням." + "uk": "Аналізатор безпеки буде використовуватися разом з режимом підтвердження. За замовчуванням він використовує передбачений LLM ризик дії для визначення, чи потрібно запитувати підтвердження у користувача. Якщо ризик ВИСОКИЙ, він запитуватиме підтвердження за замовчуванням.", + "ca": "L'analitzador de seguretat s'utilitzarà conjuntament amb el mode de confirmació. Per defecte, utilitza el risc d'acció predit per LLM per determinar si cal demanar confirmació a l'usuari. Si el risc és ALT, demanarà confirmació a l'usuari per defecte." }, "SETTINGS$DONT_KNOW_API_KEY": { "en": "Don't know your API key?", @@ -6749,7 +7170,8 @@ "fr": "Vous ne connaissez pas votre clé API ?", "tr": "API anahtarınızı bilmiyor musunuz?", "de": "Kennen Sie Ihren API-Schlüssel nicht?", - "uk": "Не знаєте свого API ключа?" + "uk": "Не знаєте свого API ключа?", + "ca": "No coneixeu la vostra clau d'API?" }, "SETTINGS$CLICK_FOR_INSTRUCTIONS": { "en": "Click here for instructions", @@ -6765,7 +7187,8 @@ "fr": "Cliquez ici pour les instructions", "tr": "Talimatlar için buraya tıklayın", "de": "Klicken Sie hier für Anweisungen", - "uk": "Натисніть тут, щоб отримати інструкції" + "uk": "Натисніть тут, щоб отримати інструкції", + "ca": "Feu clic aquí per obtenir instruccions" }, "SETTINGS$NEED_OPENHANDS_ACCOUNT": { "en": "Need an OpenHands Account?", @@ -6781,7 +7204,8 @@ "fr": "Besoin d'un compte OpenHands ?", "tr": "OpenHands hesabına mı ihtiyacınız var?", "de": "Benötigen Sie ein OpenHands-Konto?", - "uk": "Потрібен обліковий запис OpenHands?" + "uk": "Потрібен обліковий запис OpenHands?", + "ca": "Necessiteu un compte d'OpenHands?" }, "SETTINGS$CLICK_HERE": { "en": "Click here", @@ -6797,7 +7221,8 @@ "fr": "Cliquez ici", "tr": "Buraya tıklayın", "de": "Klicken Sie hier", - "uk": "Натисніть тут" + "uk": "Натисніть тут", + "ca": "Feu clic aquí" }, "SETTINGS$SAVED": { "en": "Settings saved", @@ -6813,7 +7238,8 @@ "fr": "Paramètres enregistrés", "tr": "Ayarlar kaydedildi", "de": "Einstellungen gespeichert", - "uk": "Налаштування збережено" + "uk": "Налаштування збережено", + "ca": "Configuració desada" }, "SETTINGS$SAVED_WARNING": { "en": "Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.", @@ -6829,7 +7255,8 @@ "fr": "Paramètres enregistrés. Pour les anciennes conversations, vous devrez arrêter et redémarrer la conversation pour voir les changements.", "tr": "Ayarlar kaydedildi. Eski konuşmalar için değişiklikleri görmek üzere konuşmayı durdurup yeniden başlatmanız gerekecek.", "de": "Einstellungen gespeichert. Für alte Gespräche müssen Sie das Gespräch stoppen und neu starten, um die Änderungen zu sehen.", - "uk": "Налаштування збережено. Для старих розмов вам потрібно буде зупинити та перезапустити розмову, щоб побачити зміни." + "uk": "Налаштування збережено. Для старих розмов вам потрібно буде зупинити та перезапустити розмову, щоб побачити зміни.", + "ca": "Configuració desada. Per a les converses antigues, haureu d'aturar i reiniciar la conversa per veure els canvis." }, "SETTINGS$RESET": { "en": "Settings reset", @@ -6845,7 +7272,8 @@ "fr": "Paramètres réinitialisés", "tr": "Ayarlar sıfırlandı", "de": "Einstellungen zurückgesetzt", - "uk": "Скидання налаштувань" + "uk": "Скидання налаштувань", + "ca": "Configuració restablerta" }, "SETTINGS$API_KEYS": { "en": "API Keys", @@ -6861,7 +7289,8 @@ "it": "Chiavi API", "pt": "Chaves API", "es": "Claves API", - "tr": "API Anahtarları" + "tr": "API Anahtarları", + "ca": "Claus d'API" }, "SETTINGS$API_KEYS_DESCRIPTION": { "en": "API keys allow you to authenticate with the OpenHands API programmatically. Keep your API keys secure; anyone with your API key can access your account. For more information on how to use the API, see our API documentation.", @@ -6877,7 +7306,8 @@ "fr": "Les clés API vous permettent de vous authentifier auprès de l'API OpenHands par programmation. Gardez vos clés API en sécurité ; toute personne disposant de votre clé API peut accéder à votre compte. Pour plus d'informations sur l'utilisation de l'API, consultez notre documentation API.", "tr": "API anahtarları, OpenHands API ile programlı olarak kimlik doğrulamanıza olanak tanır. API anahtarlarınızı güvende tutun; API anahtarınıza sahip olan herkes hesabınıza erişebilir. API'nin nasıl kullanılacağı hakkında daha fazla bilgi için API belgelerimize bakın.", "de": "API-Schlüssel ermöglichen es Ihnen, sich programmatisch bei der OpenHands-API zu authentifizieren. Halten Sie Ihre API-Schlüssel sicher; jeder mit Ihrem API-Schlüssel kann auf Ihr Konto zugreifen. Weitere Informationen zur Verwendung der API finden Sie in unserer API-Dokumentation.", - "uk": "Ключі API дозволяють вам програмно автентифікуватися за допомогою API OpenHands. Зберігайте свої ключі API в безпеці; будь-хто, хто має ваш ключ API, може отримати доступ до вашого облікового запису. Для отримання додаткової інформації про використання API, перегляньте нашу документацію API." + "uk": "Ключі API дозволяють вам програмно автентифікуватися за допомогою API OpenHands. Зберігайте свої ключі API в безпеці; будь-хто, хто має ваш ключ API, може отримати доступ до вашого облікового запису. Для отримання додаткової інформації про використання API, перегляньте нашу документацію API.", + "ca": "Les claus d'API us permeten autenticar-vos amb l'API d'OpenHands de manera programàtica. Manteniu les vostres claus d'API segures; qualsevol persona amb la vostra clau d'API pot accedir al vostre compte. Per obtenir més informació sobre com fer servir l'API, consulteu la nostra documentació de l'API." }, "SETTINGS$OPENHANDS_API_KEY_HELP": { "en": "You can find your OpenHands API Key in the API Keys tab of OpenHands Cloud.", @@ -6893,7 +7323,8 @@ "fr": "Vous pouvez trouver votre clé API OpenHands dans l'onglet Clés API d'OpenHands Cloud.", "tr": "OpenHands API Anahtarınızı OpenHands Cloud'un API Anahtarları sekmesinde bulabilirsiniz.", "de": "Sie finden Ihren OpenHands API-Schlüssel im Tab API-Schlüssel von OpenHands Cloud.", - "uk": "Ви можете знайти свій ключ API OpenHands у вкладці Ключі API OpenHands Cloud." + "uk": "Ви можете знайти свій ключ API OpenHands у вкладці Ключі API OpenHands Cloud.", + "ca": "Podeu trobar la vostra clau d'API d'OpenHands a la pestanya Claus d'API d'OpenHands Cloud." }, "SETTINGS$OPENHANDS_API_KEY_HELP_TEXT": { "en": "You can find your OpenHands API Key in the", @@ -6909,7 +7340,8 @@ "fr": "Vous pouvez trouver votre clé API OpenHands dans", "tr": "OpenHands API Anahtarınızı", "de": "Sie finden Ihren OpenHands API-Schlüssel im", - "uk": "Ви можете знайти свій ключ API OpenHands у" + "uk": "Ви можете знайти свій ключ API OpenHands у", + "ca": "Podeu trobar la vostra clau d'API d'OpenHands a la" }, "SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX": { "en": "tab of OpenHands Cloud.", @@ -6925,7 +7357,8 @@ "fr": "l'onglet d'OpenHands Cloud.", "tr": "OpenHands Cloud'un sekmesinde bulabilirsiniz.", "de": "Tab von OpenHands Cloud.", - "uk": "вкладці OpenHands Cloud." + "uk": "вкладці OpenHands Cloud.", + "ca": "pestanya d'OpenHands Cloud." }, "SETTINGS$LLM_BILLING_INFO": { "en": "LLM usage is billed at the providers' rates with no markup.", @@ -6941,7 +7374,8 @@ "fr": "L'utilisation de LLM est facturée aux tarifs des fournisseurs sans majoration.", "tr": "LLM kullanımı, sağlayıcıların oranlarında ek ücret olmadan faturalandırılır.", "de": "LLM-Nutzung wird zu Anbieterpreisen ohne Aufschlag abgerechnet.", - "uk": "Використання LLM оплачується за тарифами провайдерів без надбавки." + "uk": "Використання LLM оплачується за тарифами провайдерів без надбавки.", + "ca": "L'ús del LLM es factura a les tarifes dels proveïdors sense cap markup." }, "SETTINGS$LLM_ADMIN_INFO": { "en": "The LLM settings configured below will apply to all users of the organization.", @@ -6957,7 +7391,8 @@ "fr": "Les paramètres LLM configurés ci-dessous s'appliqueront à tous les utilisateurs de l'organisation.", "tr": "Aşağıda yapılandırılan LLM ayarları, kuruluştaki tüm kullanıcılara uygulanacaktır.", "de": "Die unten konfigurierten LLM-Einstellungen gelten für alle Benutzer der Organisation.", - "uk": "Налаштування LLM, налаштовані нижче, будуть застосовані до всіх користувачів організації." + "uk": "Налаштування LLM, налаштовані нижче, будуть застосовані до всіх користувачів організації.", + "ca": "La configuració LLM configurada a continuació s'aplicarà a tots els usuaris de l'organització." }, "SETTINGS$LLM_MEMBER_INFO": { "en": "LLM settings are managed by your organization's administrator.", @@ -6973,7 +7408,8 @@ "fr": "Les paramètres LLM sont gérés par l'administrateur de votre organisation.", "tr": "LLM ayarları kuruluşunuzun yöneticisi tarafından yönetilmektedir.", "de": "LLM-Einstellungen werden vom Administrator Ihrer Organisation verwaltet.", - "uk": "Налаштування LLM керуються адміністратором вашої організації." + "uk": "Налаштування LLM керуються адміністратором вашої організації.", + "ca": "La configuració LLM és gestionada per l'administrador de la vostra organització." }, "SETTINGS$SEE_PRICING_DETAILS": { "en": "See pricing details.", @@ -6989,7 +7425,8 @@ "fr": "Voir les détails de prix.", "tr": "Fiyat ayrıntılarını gör.", "de": "Preisdetails anzeigen.", - "uk": "Переглянути деталі цін." + "uk": "Переглянути деталі цін.", + "ca": "Consulteu els detalls dels preus." }, "SETTINGS$CREATE_API_KEY": { "en": "Create API Key", @@ -7005,7 +7442,8 @@ "it": "Crea chiave API", "pt": "Criar chave API", "es": "Crear clave API", - "tr": "API Anahtarı Oluştur" + "tr": "API Anahtarı Oluştur", + "ca": "Crea una clau d'API" }, "SETTINGS$CREATE_API_KEY_DESCRIPTION": { "en": "Give your API key a descriptive name to help you identify it later.", @@ -7021,7 +7459,8 @@ "it": "Dai alla tua chiave API un nome descrittivo per aiutarti a identificarla in seguito.", "pt": "Dê à sua chave API um nome descritivo para ajudá-lo a identificá-la posteriormente.", "es": "Asigne a su clave API un nombre descriptivo para ayudarle a identificarla más adelante.", - "tr": "API anahtarınıza, daha sonra tanımlamanıza yardımcı olacak açıklayıcı bir isim verin." + "tr": "API anahtarınıza, daha sonra tanımlamanıza yardımcı olacak açıklayıcı bir isim verin.", + "ca": "Doneu un nom descriptiu a la vostra clau d'API per poder-la identificar més endavant." }, "SETTINGS$DELETE_API_KEY": { "en": "Delete API Key", @@ -7037,7 +7476,8 @@ "it": "Elimina chiave API", "pt": "Excluir chave API", "es": "Eliminar clave API", - "tr": "API Anahtarını Sil" + "tr": "API Anahtarını Sil", + "ca": "Elimina la clau d'API" }, "SETTINGS$DELETE_API_KEY_CONFIRMATION": { "en": "Are you sure you want to delete the API key \"{{name}}\"? This action cannot be undone.", @@ -7053,7 +7493,8 @@ "it": "Sei sicuro di voler eliminare la chiave API \"{{name}}\"? Questa azione non può essere annullata.", "pt": "Tem certeza de que deseja excluir a chave API \"{{name}}\"? Esta ação não pode ser desfeita.", "es": "¿Está seguro de que desea eliminar la clave API \"{{name}}\"? Esta acción no se puede deshacer.", - "tr": "\"{{name}}\" API anahtarını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + "tr": "\"{{name}}\" API anahtarını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "ca": "Esteu segur que voleu eliminar la clau d'API \"{{name}}\"? Aquesta acció no es pot desfer." }, "SETTINGS$NO_API_KEYS": { "en": "You don't have any API keys yet. Create one to get started.", @@ -7069,7 +7510,8 @@ "it": "Non hai ancora chiavi API. Creane una per iniziare.", "pt": "Você ainda não tem nenhuma chave API. Crie uma para começar.", "es": "Aún no tiene ninguna clave API. Cree una para comenzar.", - "tr": "Henüz API anahtarınız yok. Başlamak için bir tane oluşturun." + "tr": "Henüz API anahtarınız yok. Başlamak için bir tane oluşturun.", + "ca": "Encara no teniu cap clau d'API. Creeu-ne una per començar." }, "SETTINGS$NAME": { "en": "Name", @@ -7085,7 +7527,8 @@ "it": "Nome", "pt": "Nome", "es": "Nombre", - "tr": "İsim" + "tr": "İsim", + "ca": "Nom" }, "SECRETS$DESCRIPTION": { "en": "Description", @@ -7101,7 +7544,8 @@ "it": "Descrizione", "pt": "Descrição", "es": "Descripción", - "tr": "Açıklama" + "tr": "Açıklama", + "ca": "Descripció" }, "SETTINGS$KEY_PREFIX": { "en": "Key Prefix", @@ -7117,7 +7561,8 @@ "it": "Prefisso chiave", "pt": "Prefixo da chave", "es": "Prefijo de clave", - "tr": "Anahtar Öneki" + "tr": "Anahtar Öneki", + "ca": "Prefix de la clau" }, "SETTINGS$CREATED_AT": { "en": "Created", @@ -7133,7 +7578,8 @@ "it": "Creato", "pt": "Criado", "es": "Creado", - "tr": "Oluşturuldu" + "tr": "Oluşturuldu", + "ca": "Creat" }, "SETTINGS$LAST_USED": { "en": "Last Used", @@ -7149,7 +7595,8 @@ "it": "Ultimo utilizzo", "pt": "Último uso", "es": "Último uso", - "tr": "Son Kullanım" + "tr": "Son Kullanım", + "ca": "Últim ús" }, "SETTINGS$ACTIONS": { "en": "Actions", @@ -7165,7 +7612,8 @@ "it": "Azioni", "pt": "Ações", "es": "Acciones", - "tr": "İşlemler" + "tr": "İşlemler", + "ca": "Accions" }, "SETTINGS$API_KEY_CREATED": { "en": "API Key Created", @@ -7181,7 +7629,8 @@ "it": "Chiave API creata", "pt": "Chave API criada", "es": "Clave API creada", - "tr": "API Anahtarı Oluşturuldu" + "tr": "API Anahtarı Oluşturuldu", + "ca": "Clau d'API creada" }, "SETTINGS$API_KEY_DELETED": { "en": "API key deleted successfully", @@ -7197,7 +7646,8 @@ "it": "Chiave API eliminata con successo", "pt": "Chave API excluída com sucesso", "es": "Clave API eliminada con éxito", - "tr": "API anahtarı başarıyla silindi" + "tr": "API anahtarı başarıyla silindi", + "ca": "La clau d'API s'ha eliminat correctament" }, "SETTINGS$API_KEY_WARNING": { "en": "This is the only time your API key will be displayed. Please copy it now and store it securely.", @@ -7213,7 +7663,8 @@ "it": "Questa è l'unica volta che la tua chiave API verrà visualizzata. Copiala ora e conservala in modo sicuro.", "pt": "Esta é a única vez que sua chave API será exibida. Por favor, copie-a agora e armazene-a com segurança.", "es": "Esta es la única vez que se mostrará su clave API. Por favor, cópiela ahora y guárdela de forma segura.", - "tr": "API anahtarınız yalnızca bu kez görüntülenecektir. Lütfen şimdi kopyalayın ve güvenli bir şekilde saklayın." + "tr": "API anahtarınız yalnızca bu kez görüntülenecektir. Lütfen şimdi kopyalayın ve güvenli bir şekilde saklayın.", + "ca": "Aquesta és l'única vegada que es mostrarà la vostra clau d'API. Copieu-la ara i guardeu-la en un lloc segur." }, "SETTINGS$API_KEY_COPIED": { "en": "API key copied to clipboard", @@ -7229,7 +7680,8 @@ "it": "Chiave API copiata negli appunti", "pt": "Chave API copiada para a área de transferência", "es": "Clave API copiada al portapapeles", - "tr": "API anahtarı panoya kopyalandı" + "tr": "API anahtarı panoya kopyalandı", + "ca": "Clau d'API copiada al porta-retalls" }, "SETTINGS$API_KEY_REFRESHED": { "en": "API key refreshed successfully", @@ -7245,7 +7697,8 @@ "it": "Chiave API aggiornata con successo", "pt": "Chave API atualizada com sucesso", "es": "Clave API actualizada con éxito", - "tr": "API anahtarı başarıyla yenilendi" + "tr": "API anahtarı başarıyla yenilendi", + "ca": "La clau d'API s'ha actualitzat correctament" }, "SETTINGS$API_KEY_NAME_PLACEHOLDER": { "en": "My API Key", @@ -7261,7 +7714,8 @@ "it": "La mia chiave API", "pt": "Minha chave API", "es": "Mi clave API", - "tr": "API Anahtarım" + "tr": "API Anahtarım", + "ca": "La meva clau d'API" }, "BUTTON$CREATE": { "en": "Create", @@ -7277,7 +7731,8 @@ "it": "Crea", "pt": "Criar", "es": "Crear", - "tr": "Oluştur" + "tr": "Oluştur", + "ca": "Crea" }, "BUTTON$DELETE": { "en": "Delete", @@ -7293,7 +7748,8 @@ "it": "Elimina", "pt": "Excluir", "es": "Eliminar", - "tr": "Sil" + "tr": "Sil", + "ca": "Elimina" }, "BUTTON$COPY_TO_CLIPBOARD": { "en": "Copy to Clipboard", @@ -7309,7 +7765,8 @@ "it": "Copia negli appunti", "pt": "Copiar para a área de transferência", "es": "Copiar al portapapeles", - "tr": "Panoya Kopyala" + "tr": "Panoya Kopyala", + "ca": "Copia al porta-retalls" }, "BUTTON$HOME": { "en": "Home", @@ -7325,7 +7782,8 @@ "fr": "Accueil", "tr": "Ana Sayfa", "de": "Startseite", - "uk": "Головна" + "uk": "Головна", + "ca": "Inici" }, "BUTTON$OPEN_IN_NEW_TAB": { "en": "Open in New Tab", @@ -7341,7 +7799,8 @@ "fr": "Ouvrir dans un nouvel onglet", "tr": "Yeni Sekmede Aç", "de": "In neuem Tab öffnen", - "uk": "Відкрити в новій вкладці" + "uk": "Відкрити в новій вкладці", + "ca": "Obre en una pestanya nova" }, "BUTTON$REFRESH": { "en": "Refresh", @@ -7357,7 +7816,8 @@ "fr": "Rafraîchir", "tr": "Yenile", "de": "Aktualisieren", - "uk": "Оновити" + "uk": "Оновити", + "ca": "Actualitza" }, "ERROR$REQUIRED_FIELD": { "en": "This field is required", @@ -7373,7 +7833,8 @@ "it": "Questo campo è obbligatorio", "pt": "Este campo é obrigatório", "es": "Este campo es obligatorio", - "tr": "Bu alan zorunludur" + "tr": "Bu alan zorunludur", + "ca": "Aquest camp és obligatori" }, "PLANNER$EMPTY_MESSAGE": { "en": "There is currently no plan for this repo", @@ -7389,7 +7850,8 @@ "it": "Attualmente non c'è un piano per questo repository", "pt": "Atualmente não há plano para este repositório", "es": "Actualmente no hay un plan para este repositorio", - "tr": "Şu anda bu depo için bir plan yok" + "tr": "Şu anda bu depo için bir plan yok", + "ca": "Actualment no hi ha cap pla per a aquest repositori" }, "SIDEBAR$NAVIGATION_LABEL": { "en": "Sidebar navigation", @@ -7405,7 +7867,8 @@ "fr": "Navigation de la barre latérale", "tr": "Kenar çubuğu gezintisi", "ja": "サイドバーのナビゲーション", - "uk": "Навігація бічної панелі" + "uk": "Навігація бічної панелі", + "ca": "Navegació de la barra lateral" }, "FEEDBACK$PUBLIC_LABEL": { "en": "Public", @@ -7421,7 +7884,8 @@ "fr": "Public", "tr": "Herkese Açık", "ja": "公開", - "uk": "Загальнодоступний" + "uk": "Загальнодоступний", + "ca": "Públic" }, "FEEDBACK$PRIVATE_LABEL": { "en": "Private", @@ -7437,7 +7901,8 @@ "fr": "Privé", "tr": "Özel", "ja": "非公開", - "uk": "Приватний" + "uk": "Приватний", + "ca": "Privat" }, "SIDEBAR$CONVERSATIONS": { "en": "Conversations", @@ -7453,7 +7918,8 @@ "ar": "المحادثات", "fr": "Conversations", "tr": "Konuşmalar", - "uk": "Розмови" + "uk": "Розмови", + "ca": "Converses" }, "STATUS$CONNECTING_TO_RUNTIME": { "en": "Connecting to runtime...", @@ -7469,7 +7935,8 @@ "fr": "Connexion à l'environnement d'exécution en cours...", "tr": "Çalışma zamanı ortamına bağlanılıyor...", "ja": "ランタイムに接続中", - "uk": "Підключення до середовища виконання..." + "uk": "Підключення до середовища виконання...", + "ca": "Connectant a l'entorn d'execució..." }, "STATUS$STARTING_RUNTIME": { "en": "Starting runtime... (this may take 1-2 minutes)", @@ -7485,7 +7952,8 @@ "fr": "Démarrage de l'environnement d'exécution... (cela peut prendre 1-2 minutes)", "tr": "Çalışma zamanı başlatılıyor... (bu 1-2 dakika sürebilir)", "ja": "ランタイムを開始中...(1-2分かかる場合があります)", - "uk": "Запуск середовища виконання... (це може зайняти 1-2 хвилини)" + "uk": "Запуск середовища виконання... (це може зайняти 1-2 хвилини)", + "ca": "Iniciant l'entorn d'execució... (això pot trigar 1-2 minuts)" }, "STATUS$SETTING_UP_WORKSPACE": { "en": "Setting up workspace...", @@ -7501,7 +7969,8 @@ "fr": "Configuration de l'espace de travail...", "tr": "Çalışma alanı ayarlanıyor...", "ja": "ワークスペースを設定中...", - "uk": "Налаштування робочого простору..." + "uk": "Налаштування робочого простору...", + "ca": "Configurant l'espai de treball..." }, "STATUS$SETTING_UP_GIT_HOOKS": { "en": "Setting up git hooks...", @@ -7517,7 +7986,8 @@ "fr": "Configuration des hooks git...", "tr": "Git kancaları ayarlanıyor...", "ja": "git フックを設定中...", - "uk": "Налаштування git-хуків..." + "uk": "Налаштування git-хуків...", + "ca": "Configurant els hooks de Git..." }, "STATUS$SETTING_UP_SKILLS": { "en": "Setting up skills...", @@ -7533,7 +8003,8 @@ "fr": "Configuration des compétences...", "tr": "Yetenekler ayarlanıyor...", "ja": "スキルを設定中...", - "uk": "Налаштування навичок..." + "uk": "Налаштування навичок...", + "ca": "Configurant les habilitats..." }, "ACCOUNT_SETTINGS_MODAL$DISCONNECT": { "en": "Disconnect", @@ -7549,7 +8020,8 @@ "it": "Disconnetti", "pt": "Desconectar", "tr": "Bağlantıyı kes", - "uk": "Відключитися" + "uk": "Відключитися", + "ca": "Desconnecta" }, "ACCOUNT_SETTINGS_MODAL$SAVE": { "en": "Save", @@ -7565,7 +8037,8 @@ "it": "Salva", "pt": "Salvar", "tr": "Kaydet", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "ACCOUNT_SETTINGS_MODAL$CLOSE": { "en": "Close", @@ -7581,7 +8054,8 @@ "it": "Chiudi", "pt": "Fechar", "tr": "Kapat", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID": { "en": "GitHub token is invalid. Please try again.", @@ -7597,7 +8071,8 @@ "it": "Il token GitHub non è valido. Per favore riprova.", "pt": "O token do GitHub é inválido. Por favor, tente novamente.", "tr": "GitHub token'ı geçersiz. Lütfen tekrar deneyin.", - "uk": "Токен GitHub недійсний. Спробуйте ще раз." + "uk": "Токен GitHub недійсний. Спробуйте ще раз.", + "ca": "El token de GitHub no és vàlid. Torneu-ho a intentar." }, "CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN": { "en": "Get your token", @@ -7613,7 +8088,8 @@ "it": "Ottieni il tuo token", "pt": "Obtenha seu token", "tr": "Token'ınızı alın", - "uk": "Отримайте свій токен" + "uk": "Отримайте свій токен", + "ca": "Obteniu el vostre token" }, "CONNECT_TO_GITHUB_MODAL$HERE": { "en": "here", @@ -7629,7 +8105,8 @@ "it": "qui", "pt": "aqui", "tr": "burada", - "uk": "тут" + "uk": "тут", + "ca": "aquí" }, "CONNECT_TO_GITHUB_MODAL$CONNECT": { "en": "Connect", @@ -7645,7 +8122,8 @@ "tr": "Bağlan", "ko-KR": "연결", "ja": "接続", - "uk": "Підключитися" + "uk": "Підключитися", + "ca": "Connecta" }, "CONNECT_TO_GITHUB_MODAL$CLOSE": { "en": "Close", @@ -7661,7 +8139,8 @@ "it": "Chiudi", "pt": "Fechar", "tr": "Kapat", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$BY_CONNECTING_YOU_AGREE": { "en": "By connecting you agree to our", @@ -7677,7 +8156,8 @@ "it": "Connettendoti accetti i nostri", "pt": "Ao conectar você concorda com nossos", "tr": "Bağlanarak kabul etmiş olursunuz", - "uk": "Підключаючись, ви погоджуєтеся з нашими" + "uk": "Підключаючись, ви погоджуєтеся з нашими", + "ca": "En connectar-vos, accepteu les nostres" }, "CONNECT_TO_GITHUB_BY_TOKEN_MODAL$CONTINUE": { "en": "Continue", @@ -7693,7 +8173,8 @@ "it": "Continua", "pt": "Continuar", "tr": "Devam et", - "uk": "Продовжити" + "uk": "Продовжити", + "ca": "Continua" }, "LOADING_PROJECT$LOADING": { "en": "Loading...", @@ -7709,7 +8190,8 @@ "it": "Caricamento...", "pt": "Carregando...", "tr": "Yükleniyor...", - "uk": "Завантаження..." + "uk": "Завантаження...", + "ca": "Carregant..." }, "CUSTOM_INPUT$OPTIONAL_LABEL": { "en": "(Optional)", @@ -7725,7 +8207,8 @@ "it": "(Opzionale)", "pt": "(Opcional)", "tr": "(İsteğe bağlı)", - "uk": "(Необов'язково)" + "uk": "(Необов'язково)", + "ca": "(Opcional)" }, "SETTINGS_FORM$CUSTOM_MODEL_LABEL": { "en": "Custom Model", @@ -7741,7 +8224,8 @@ "it": "Modello personalizzato", "pt": "Modelo personalizado", "tr": "Özel model", - "uk": "Власна модель" + "uk": "Власна модель", + "ca": "Model personalitzat" }, "SETTINGS_FORM$BASE_URL_LABEL": { "en": "Base URL", @@ -7757,7 +8241,8 @@ "it": "URL di base", "pt": "URL base", "tr": "Temel URL", - "uk": "Базовий URL" + "uk": "Базовий URL", + "ca": "URL base" }, "SETTINGS_FORM$API_KEY_LABEL": { "en": "API Key", @@ -7773,7 +8258,8 @@ "it": "Chiave API", "pt": "Chave API", "tr": "API Anahtarı", - "uk": "Ключ API" + "uk": "Ключ API", + "ca": "Clau d'API" }, "SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL": { "en": "Don't know your API key?", @@ -7789,7 +8275,8 @@ "it": "Non conosci la tua chiave API?", "pt": "Não sabe sua chave API?", "tr": "API anahtarınızı bilmiyor musunuz?", - "uk": "Не знаєте свого API-ключа?" + "uk": "Не знаєте свого API-ключа?", + "ca": "No coneixeu la vostra clau d'API?" }, "SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL": { "en": "Click here for instructions", @@ -7805,7 +8292,8 @@ "it": "Clicca qui per le istruzioni", "pt": "Clique aqui para instruções", "tr": "Talimatlar için buraya tıklayın", - "uk": "Натисніть тут, щоб отримати інструкції" + "uk": "Натисніть тут, щоб отримати інструкції", + "ca": "Feu clic aquí per obtenir instruccions" }, "SETTINGS_FORM$AGENT_LABEL": { "en": "Agent", @@ -7821,7 +8309,8 @@ "it": "Agente", "pt": "Agente", "tr": "Ajan", - "uk": "Агент" + "uk": "Агент", + "ca": "Agent" }, "SETTINGS_FORM$SECURITY_ANALYZER_LABEL": { "en": "Security Analyzer (Optional)", @@ -7837,7 +8326,8 @@ "it": "Analizzatore di sicurezza (Opzionale)", "pt": "Analisador de segurança (Opcional)", "tr": "Güvenlik Analizörü (İsteğe bağlı)", - "uk": "Аналізатор безпеки (необов'язково)" + "uk": "Аналізатор безпеки (необов'язково)", + "ca": "Analitzador de seguretat (opcional)" }, "SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL": { "en": "Enable Confirmation Mode", @@ -7853,7 +8343,8 @@ "it": "Abilita modalità di conferma", "pt": "Ativar modo de confirmação", "tr": "Onay modunu etkinleştir", - "uk": "Увімкнути режим підтвердження" + "uk": "Увімкнути режим підтвердження", + "ca": "Activa el mode de confirmació" }, "SETTINGS_FORM$SAVE_LABEL": { "en": "Save", @@ -7869,7 +8360,8 @@ "it": "Salva", "pt": "Salvar", "tr": "Kaydet", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "SETTINGS_FORM$CLOSE_LABEL": { "en": "Close", @@ -7885,7 +8377,8 @@ "it": "Chiudi", "pt": "Fechar", "tr": "Kapat", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "SETTINGS_FORM$CANCEL_LABEL": { "en": "Cancel", @@ -7901,7 +8394,8 @@ "it": "Annulla", "pt": "Cancelar", "tr": "İptal", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "SETTINGS_FORM$END_SESSION_LABEL": { "en": "End Session", @@ -7917,7 +8411,8 @@ "it": "Termina sessione", "pt": "Encerrar sessão", "tr": "Oturumu sonlandır", - "uk": "Закінчити сеанс" + "uk": "Закінчити сеанс", + "ca": "Finalitza la sessió" }, "SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE": { "en": "Changing your settings will clear your workspace and start a new session. Are you sure you want to continue?", @@ -7933,7 +8428,8 @@ "it": "La modifica delle impostazioni cancellerà il tuo spazio di lavoro e avvierà una nuova sessione. Sei sicuro di voler continuare?", "pt": "Alterar suas configurações limpará seu espaço de trabalho e iniciará uma nova sessão. Tem certeza de que deseja continuar?", "tr": "Ayarlarınızı değiştirmek çalışma alanınızı temizleyecek ve yeni bir oturum başlatacak. Devam etmek istediğinizden emin misiniz?", - "uk": "Зміна налаштувань очистить вашу робочу область та розпочне новий сеанс. Ви впевнені, що хочете продовжити?" + "uk": "Зміна налаштувань очистить вашу робочу область та розпочне новий сеанс. Ви впевнені, що хочете продовжити?", + "ca": "Canviar la configuració buidarà l'espai de treball i iniciarà una nova sessió. Esteu segur que voleu continuar?" }, "SETTINGS_FORM$ARE_YOU_SURE_LABEL": { "en": "Are you sure?", @@ -7949,7 +8445,8 @@ "it": "Sei sicuro?", "pt": "Tem certeza?", "tr": "Emin misiniz?", - "uk": "Ви впевнені?" + "uk": "Ви впевнені?", + "ca": "Esteu segur?" }, "SETTINGS_FORM$ALL_INFORMATION_WILL_BE_DELETED_MESSAGE": { "en": "All saved information in your AI settings will be deleted, including any API keys.", @@ -7965,7 +8462,8 @@ "it": "Tutte le informazioni salvate nelle tue impostazioni AI saranno eliminate, incluse le chiavi API.", "pt": "Todas as informações salvas nas suas configurações de IA serão excluídas, incluindo quaisquer chaves API.", "tr": "API anahtarları dahil olmak üzere AI ayarlarınızdaki tüm kayıtlı bilgiler silinecektir.", - "uk": "Усю збережену інформацію у ваших налаштуваннях штучного інтелекту буде видалено, включаючи будь-які ключі API." + "uk": "Усю збережену інформацію у ваших налаштуваннях штучного інтелекту буде видалено, включаючи будь-які ключі API.", + "ca": "Tota la informació desada a la configuració de la IA s'eliminarà, incloses les claus d'API." }, "PROJECT_MENU_DETAILS_PLACEHOLDER$NEW_PROJECT_LABEL": { "en": "New Project", @@ -7981,7 +8479,8 @@ "it": "Nuovo progetto", "pt": "Novo projeto", "tr": "Yeni proje", - "uk": "Новий проект" + "uk": "Новий проект", + "ca": "Nou projecte" }, "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB": { "en": "Connect to GitHub", @@ -7997,7 +8496,8 @@ "pt": "Conectar ao GitHub", "es": "Conectar a GitHub", "tr": "GitHub'a bağlan", - "uk": "Підключитися до GitHub" + "uk": "Підключитися до GitHub", + "ca": "Connecta a GitHub" }, "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED": { "en": "Connected", @@ -8013,7 +8513,8 @@ "pt": "Conectado", "es": "Conectado", "tr": "Bağlandı", - "uk": "Підключено" + "uk": "Підключено", + "ca": "Connectat" }, "PROJECT_MENU_DETAILS$AGO_LABEL": { "en": "ago", @@ -8029,7 +8530,8 @@ "it": "fa", "pt": "atrás", "tr": "önce", - "uk": "тому" + "uk": "тому", + "ca": "fa" }, "STATUS$ERROR_LLM_AUTHENTICATION": { "en": "Error authenticating with the LLM provider. Please check your API key", @@ -8045,7 +8547,8 @@ "it": "Errore di autenticazione con il provider LLM. Controlla la tua chiave API", "pt": "Erro ao autenticar com o provedor LLM. Por favor, verifique sua chave API", "tr": "LLM sağlayıcısı ile kimlik doğrulama hatası. Lütfen API anahtarınızı kontrol edin", - "uk": "Помилка автентифікації у постачальника LLM. Перевірте свій ключ API." + "uk": "Помилка автентифікації у постачальника LLM. Перевірте свій ключ API.", + "ca": "Error d'autenticació amb el proveïdor de LLM. Comproveu la vostra clau d'API" }, "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE": { "en": "The LLM provider is currently unavailable. Please try again later.", @@ -8061,7 +8564,8 @@ "it": "Il provider LLM non è attualmente disponibile. Per favore, riprova più tardi.", "pt": "O provedor LLM não está atualmente disponível. Por favor, tente novamente mais tarde.", "tr": "LLM sağlayıcısı şu anda kullanılamıyor. Lütfen daha sonra tekrar deneyin.", - "uk": "Постачальник LLM наразі недоступний. Будь ласка, спробуйте пізніше." + "uk": "Постачальник LLM наразі недоступний. Будь ласка, спробуйте пізніше.", + "ca": "El proveïdor de LLM no està disponible actualment. Torneu-ho a intentar més tard." }, "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR": { "en": "The request failed with an internal server error.", @@ -8077,7 +8581,8 @@ "it": "Si è verificato un errore durante la connessione al runtime. Aggiorna la pagina.", "pt": "Ocorreu um erro ao conectar ao ambiente de execução. Por favor, atualize a página.", "tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin.", - "uk": "Запит не вдалося виконати через внутрішню помилку сервера." + "uk": "Запит не вдалося виконати через внутрішню помилку сервера.", + "ca": "La sol·licitud ha fallat amb un error intern del servidor." }, "STATUS$ERROR_LLM_OUT_OF_CREDITS": { "en": "You're out of OpenHands Credits. Add funds", @@ -8093,7 +8598,8 @@ "fr": "Vous n'avez plus de crédits OpenHands. Ajouter des fonds", "tr": "OpenHands kredileriniz tükendi. Bakiye ekle", "de": "Ihre OpenHands-Guthaben sind aufgebraucht. Guthaben hinzufügen", - "uk": "У вас закінчилися кредити OpenHands. Додати кошти" + "uk": "У вас закінчилися кредити OpenHands. Додати кошти", + "ca": "No teniu crèdits d'OpenHands. Afegiu fons" }, "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION": { "en": "Content policy violation. The output was blocked by content filtering policy.", @@ -8109,7 +8615,8 @@ "ar": "انتهاك سياسة المحتوى. تم حظر المخرجات بواسطة سياسة تصفية المحتوى.", "fr": "Violation de la politique de contenu. La sortie a été bloquée par la politique de filtrage de contenu.", "tr": "İçerik politikası ihlali. Çıktı, içerik filtreleme politikası tarafından engellendi.", - "uk": "Порушення політики щодо вмісту. Вивід було заблоковано політикою фільтрації вмісту." + "uk": "Порушення політики щодо вмісту. Вивід було заблоковано політикою фільтрації вмісту.", + "ca": "Infracció de la política de contingut. La sortida ha estat bloquejada per la política de filtratge de contingut." }, "STATUS$ERROR": { "en": "An error occurred. Please try again.", @@ -8125,7 +8632,8 @@ "pt": "Ocorreu um erro. Por favor, tente novamente.", "es": "Ocurrió un error. Por favor, inténtalo de nuevo.", "tr": "Bir hata oluştu. Lütfen tekrar deneyin.", - "uk": "Сталася помилка. Будь ласка, спробуйте ще раз." + "uk": "Сталася помилка. Будь ласка, спробуйте ще раз.", + "ca": "S'ha produït un error. Torneu-ho a intentar." }, "STATUS$ERROR_RUNTIME_DISCONNECTED": { "en": "There was an error while connecting to the runtime. Please refresh the page.", @@ -8141,7 +8649,8 @@ "pt": "Ocorreu um erro ao conectar ao ambiente de execução. Por favor, atualize a página.", "es": "Hubo un error al conectar con el entorno de ejecución. Por favor, actualice la página.", "tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin.", - "uk": "Під час підключення до середовища виконання сталася помилка. Оновіть сторінку." + "uk": "Під час підключення до середовища виконання сталася помилка. Оновіть сторінку.", + "ca": "S'ha produït un error en connectar a l'entorn d'execució. Actualitzeu la pàgina." }, "STATUS$ERROR_MEMORY": { "en": "Memory error occurred. Please try reducing the workload or restarting.", @@ -8157,7 +8666,8 @@ "pt": "Ocorreu um erro de memória. Tente reduzir a carga de trabalho ou reiniciar.", "es": "Ocurrió un error de memoria. Intenta reducir la carga de trabajo o reiniciar.", "tr": "Bellek hatası oluştu. Lütfen iş yükünü azaltmayı veya yeniden başlatmayı deneyin.", - "uk": "Сталася помилка пам'яті. Спробуйте зменшити навантаження або перезапустити." + "uk": "Сталася помилка пам'яті. Спробуйте зменшити навантаження або перезапустити.", + "ca": "S'ha produït un error de memòria. Intenteu reduir la càrrega de treball o reiniciar." }, "STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR": { "en": "Error authenticating with the Git provider. Please check your credentials.", @@ -8173,7 +8683,8 @@ "pt": "Erro ao autenticar com o provedor Git. Por favor, verifique suas credenciais.", "es": "Error al autenticar con el proveedor Git. Por favor, verifica tus credenciales.", "tr": "Git sağlayıcısı ile kimlik doğrulama hatası. Lütfen kimlik bilgilerinizi kontrol edin.", - "uk": "Помилка автентифікації у постачальника Git. Перевірте свої облікові дані." + "uk": "Помилка автентифікації у постачальника Git. Перевірте свої облікові дані.", + "ca": "Error d'autenticació amb el proveïdor de Git. Comproveu les vostres credencials." }, "STATUS$LLM_RETRY": { "en": "Retrying LLM request", @@ -8189,7 +8700,8 @@ "it": "Ritenta la richiesta LLM", "pt": "Reintentando a solicitação LLM", "tr": "LLM isteğini yeniden deniyor", - "uk": "Повторна спроба запиту LLM" + "uk": "Повторна спроба запиту LLM", + "ca": "Reintentant la sol·licitud al LLM" }, "AGENT_ERROR$BAD_ACTION": { "en": "Agent tried to execute a malformed action.", @@ -8205,7 +8717,8 @@ "pt": "O agente tentou executar uma ação malformada", "es": "El agente intentó ejecutar una acción malformada", "tr": "Ajan hatalı bir eylem gerçekleştirmeye çalıştı", - "uk": "Агент спробував виконати невірну дію." + "uk": "Агент спробував виконати невірну дію.", + "ca": "L'agent ha intentat executar una acció mal formada." }, "AGENT_ERROR$ACTION_TIMEOUT": { "en": "Action timed out.", @@ -8221,7 +8734,8 @@ "pt": "Ação expirou", "es": "La acción expiró", "tr": "İşlem zaman aşımına uğradı", - "uk": "Час дії вичерпано." + "uk": "Час дії вичерпано.", + "ca": "L'acció ha superat el temps d'espera." }, "AGENT_ERROR$TOO_MANY_CONVERSATIONS": { "en": "Too many conversations at once.", @@ -8237,7 +8751,8 @@ "it": "Troppe conversazioni contemporaneamente.", "pt": "Muitas conversas ao mesmo tempo.", "es": "Demasiadas conversaciones a la vez.", - "tr": "Aynı anda çok fazla konuşma var." + "tr": "Aynı anda çok fazla konuşma var.", + "ca": "Hi ha massa converses alhora." }, "PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL": { "en": "Connect to GitHub", @@ -8253,7 +8768,8 @@ "it": "Connetti a GitHub", "pt": "Conectar ao GitHub", "tr": "GitHub'a bağlan", - "uk": "Підключитися до GitHub" + "uk": "Підключитися до GitHub", + "ca": "Connecta a GitHub" }, "PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL": { "en": "Push to GitHub", @@ -8269,7 +8785,8 @@ "it": "Invia a GitHub", "pt": "Enviar para GitHub", "tr": "GitHub'a gönder", - "uk": "Надіслати на GitHub" + "uk": "Надіслати на GitHub", + "ca": "Publica a GitHub" }, "PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL": { "en": "Download files", @@ -8285,7 +8802,8 @@ "it": "Scarica file", "pt": "Baixar arquivos", "tr": "Dosyaları indir", - "uk": "Завантаження файлів" + "uk": "Завантаження файлів", + "ca": "Descarrega els fitxers" }, "PROJECT_MENU_CARD$OPEN": { "en": "Open project menu", @@ -8301,7 +8819,8 @@ "pt": "Abrir menu do projeto", "es": "Abrir menú del proyecto", "tr": "Proje menüsünü aç", - "uk": "Відкрити меню проекту" + "uk": "Відкрити меню проекту", + "ca": "Obre el menú del projecte" }, "ACTION_BUTTON$RESUME": { "en": "Resume the agent task", @@ -8317,7 +8836,8 @@ "pt": "Retomar a tarefa do agente", "es": "Reanudar la tarea del agente", "tr": "Ajan görevine devam et", - "uk": "Відновити завдання агента" + "uk": "Відновити завдання агента", + "ca": "Reprèn la tasca de l'agent" }, "BROWSER$SCREENSHOT_ALT": { "en": "Browser Screenshot", @@ -8333,7 +8853,8 @@ "pt": "Captura de tela do navegador", "es": "Captura de pantalla del navegador", "tr": "Tarayıcı ekran görüntüsü", - "uk": "Знімок екрана браузера" + "uk": "Знімок екрана браузера", + "ca": "Captura de pantalla del navegador" }, "ERROR_TOAST$CLOSE_BUTTON_LABEL": { "en": "Close", @@ -8349,7 +8870,8 @@ "pt": "Fechar", "es": "Cerrar", "tr": "Kapat", - "uk": "Закрити" + "uk": "Закрити", + "ca": "Tanca" }, "FILE_EXPLORER$UPLOAD": { "en": "Upload File", @@ -8365,7 +8887,8 @@ "pt": "Enviar arquivo", "es": "Subir archivo", "tr": "Dosya yükle", - "uk": "Завантажити файл" + "uk": "Завантажити файл", + "ca": "Carrega un fitxer" }, "ACTION_MESSAGE$RUN": { "en": "Running {{command}}", @@ -8381,7 +8904,8 @@ "pt": "Executando {{command}}", "es": "Ejecutando {{command}}", "tr": "{{command}} çalıştırılıyor", - "uk": "Виконую {{command}}" + "uk": "Виконую {{command}}", + "ca": "S'executa {{command}}" }, "ACTION_MESSAGE$RUN_IPYTHON": { "en": "Running a Python command", @@ -8397,7 +8921,8 @@ "pt": "Executando um comando Python", "es": "Ejecutando un comando Python", "tr": "Python komutu çalıştırılıyor", - "uk": "Виконання команди Python" + "uk": "Виконання команди Python", + "ca": "S'executa una comanda Python" }, "ACTION_MESSAGE$CALL_TOOL_MCP": { "en": "Calling MCP Tool: {{mcp_tool_name}}", @@ -8413,7 +8938,8 @@ "pt": "Chamando ferramenta MCP: {{mcp_tool_name}}", "es": "Llamando a la herramienta MCP: {{mcp_tool_name}}", "tr": "MCP Aracı çağrılıyor: {{mcp_tool_name}}", - "uk": "Викликаю інструмент MCP: {{mcp_tool_name}}" + "uk": "Викликаю інструмент MCP: {{mcp_tool_name}}", + "ca": "S'invoca l'eina MCP: {{mcp_tool_name}}" }, "ACTION_MESSAGE$READ": { "en": "Reading {{path}}", @@ -8429,7 +8955,8 @@ "pt": "Lendo {{path}}", "es": "Leyendo {{path}}", "tr": "{{path}} okunuyor", - "uk": "Читаю {{path}}" + "uk": "Читаю {{path}}", + "ca": "S'llegeix {{path}}" }, "ACTION_MESSAGE$EDIT": { "en": "Editing {{path}}", @@ -8445,7 +8972,8 @@ "pt": "Editando {{path}}", "es": "Editando {{path}}", "tr": "{{path}} düzenleniyor", - "uk": "Редагую {{path}}" + "uk": "Редагую {{path}}", + "ca": "S'edita {{path}}" }, "ACTION_MESSAGE$WRITE": { "en": "Writing to {{path}}", @@ -8461,7 +8989,8 @@ "pt": "Escrevendo em {{path}}", "es": "Escribiendo en {{path}}", "tr": "{{path}} dosyasına yazılıyor", - "uk": "Записую в {{path}}" + "uk": "Записую в {{path}}", + "ca": "S'escriu a {{path}}" }, "ACTION_MESSAGE$BROWSE": { "en": "Browsing the web", @@ -8477,7 +9006,8 @@ "pt": "Navegando na web", "es": "Navegando en la web", "tr": "Web'de geziniyor", - "uk": "Перегляд веб-сторінок" + "uk": "Перегляд веб-сторінок", + "ca": "Navegant per la web" }, "ACTION_MESSAGE$BROWSE_INTERACTIVE": { "en": "Interactive browsing in progress...", @@ -8493,7 +9023,8 @@ "pt": "Navegação interativa em andamento...", "es": "Navegación interactiva en progreso...", "tr": "Etkileşimli tarama devam ediyor...", - "uk": "Триває інтерактивний перегляд..." + "uk": "Триває інтерактивний перегляд...", + "ca": "Navegació interactiva en curs..." }, "ACTION_MESSAGE$THINK": { "en": "Thinking", @@ -8509,7 +9040,8 @@ "pt": "Pensando", "es": "Pensando", "tr": "Düşünüyor", - "uk": "Розмірковую" + "uk": "Розмірковую", + "ca": "Pensant" }, "ACTION_MESSAGE$SYSTEM": { "en": "System Message", @@ -8525,7 +9057,8 @@ "pt": "Mensagem do Sistema", "es": "Mensaje del Sistema", "tr": "Sistem Mesajı", - "uk": "Системне повідомлення" + "uk": "Системне повідомлення", + "ca": "Missatge del sistema" }, "ACTION_MESSAGE$CONDENSATION": { "en": "Condensation", @@ -8541,7 +9074,8 @@ "pt": "Condensação", "es": "Condensación", "tr": "Yoğunlaşma", - "uk": "Конденсація" + "uk": "Конденсація", + "ca": "Condensació" }, "ACTION_MESSAGE$TASK_TRACKING": { "en": "Managing tasks", @@ -8557,7 +9091,8 @@ "pt": "Gerenciando tarefas", "es": "Gestionando tareas", "tr": "Görevleri yönetiyor", - "uk": "Керування завданнями" + "uk": "Керування завданнями", + "ca": "Gestionant tasques" }, "ACTION_MESSAGE$GREP": { "en": "Search in files: {{pattern}}", @@ -8573,7 +9108,8 @@ "fr": "Rechercher dans les fichiers: {{pattern}}", "tr": "Dosyalarda ara: {{pattern}}", "de": "In Dateien suchen: {{pattern}}", - "uk": "Пошук у файлах: {{pattern}}" + "uk": "Пошук у файлах: {{pattern}}", + "ca": "Cerca als fitxers: {{pattern}}" }, "ACTION_MESSAGE$GLOB": { "en": "Search files: {{pattern}}", @@ -8589,7 +9125,8 @@ "fr": "Rechercher des fichiers: {{pattern}}", "tr": "Dosya ara: {{pattern}}", "de": "Dateien suchen: {{pattern}}", - "uk": "Пошук файлів: {{pattern}}" + "uk": "Пошук файлів: {{pattern}}", + "ca": "Cerca fitxers: {{pattern}}" }, "OBSERVATION_MESSAGE$RUN": { "en": "Ran {{command}}", @@ -8605,7 +9142,8 @@ "pt": "Executou {{command}}", "es": "Ejecutó {{command}}", "tr": "{{command}} çalıştırıldı", - "uk": "Запустив {{command}}" + "uk": "Запустив {{command}}", + "ca": "S'ha executat {{command}}" }, "OBSERVATION_MESSAGE$RUN_IPYTHON": { "en": "Ran a Python command", @@ -8621,7 +9159,8 @@ "pt": "Executou um comando Python", "es": "Ejecutó un comando Python", "tr": "Python komutu çalıştırıldı", - "uk": "Виконав команду Python" + "uk": "Виконав команду Python", + "ca": "S'ha executat una comanda Python" }, "OBSERVATION_MESSAGE$READ": { "en": "Read {{path}}", @@ -8637,7 +9176,8 @@ "pt": "Leu {{path}}", "es": "Leyó {{path}}", "tr": "{{path}} okundu", - "uk": "Прочитав {{path}}" + "uk": "Прочитав {{path}}", + "ca": "S'ha llegit {{path}}" }, "OBSERVATION_MESSAGE$EDIT": { "en": "Edited {{path}}", @@ -8653,7 +9193,8 @@ "pt": "Editou {{path}}", "es": "Editó {{path}}", "tr": "{{path}} düzenlendi", - "uk": "Відредагував {{path}}" + "uk": "Відредагував {{path}}", + "ca": "S'ha editat {{path}}" }, "OBSERVATION_MESSAGE$WRITE": { "en": "Wrote to {{path}}", @@ -8669,7 +9210,8 @@ "pt": "Escreveu em {{path}}", "es": "Escribió en {{path}}", "tr": "{{path}} dosyasına yazıldı", - "uk": "Записав на {{path}}" + "uk": "Записав на {{path}}", + "ca": "S'ha escrit a {{path}}" }, "OBSERVATION_MESSAGE$BROWSE": { "en": "Browsing completed", @@ -8685,7 +9227,8 @@ "pt": "Navegação concluída", "es": "Navegación completada", "tr": "Gezinme tamamlandı", - "uk": "Перегляд завершено" + "uk": "Перегляд завершено", + "ca": "Navegació completada" }, "OBSERVATION_MESSAGE$MCP": { "en": "MCP Tool Result: {{mcp_tool_name}}", @@ -8701,7 +9244,8 @@ "pt": "Resultado da ferramenta MCP: {{mcp_tool_name}}", "es": "Resultado de la herramienta MCP: {{mcp_tool_name}}", "tr": "MCP Aracı Sonucu: {{mcp_tool_name}}", - "uk": "Результат інструменту MCP: {{mcp_tool_name}}" + "uk": "Результат інструменту MCP: {{mcp_tool_name}}", + "ca": "Resultat de l'eina MCP: {{mcp_tool_name}}" }, "OBSERVATION_MESSAGE$RECALL": { "en": "Microagent ready", @@ -8717,7 +9261,8 @@ "fr": "Microagent prêt", "tr": "MikroAjan hazır", "de": "Microagent bereit", - "uk": "Мікроагент готовий" + "uk": "Мікроагент готовий", + "ca": "Microagent preparat" }, "OBSERVATION_MESSAGE$THINK": { "en": "Thought", @@ -8733,7 +9278,8 @@ "fr": "Pensée", "tr": "Düşünce", "de": "Gedanke", - "uk": "Думка" + "uk": "Думка", + "ca": "Pensament" }, "OBSERVATION_MESSAGE$GLOB": { "en": "Search files: {{pattern}}", @@ -8749,7 +9295,8 @@ "fr": "Rechercher des fichiers: {{pattern}}", "tr": "Dosya ara: {{pattern}}", "de": "Dateien suchen: {{pattern}}", - "uk": "Пошук файлів: {{pattern}}" + "uk": "Пошук файлів: {{pattern}}", + "ca": "Cerca fitxers: {{pattern}}" }, "OBSERVATION_MESSAGE$GREP": { "en": "Search in files: {{pattern}}", @@ -8765,7 +9312,8 @@ "fr": "Rechercher dans les fichiers: {{pattern}}", "tr": "Dosyalarda ara: {{pattern}}", "de": "In Dateien suchen: {{pattern}}", - "uk": "Пошук у файлах: {{pattern}}" + "uk": "Пошук у файлах: {{pattern}}", + "ca": "Cerca als fitxers: {{pattern}}" }, "OBSERVATION_MESSAGE$TASK_TRACKING_PLAN": { "en": "Agent updated the plan", @@ -8781,7 +9329,8 @@ "pt": "O agente atualizou o plano", "es": "El agente actualizó el plan", "tr": "Ajan planı güncelledi", - "uk": "Агент оновив план" + "uk": "Агент оновив план", + "ca": "L'agent ha actualitzat el pla" }, "OBSERVATION_MESSAGE$TASK_TRACKING_VIEW": { "en": "Agent checked the current plan", @@ -8797,7 +9346,8 @@ "pt": "O agente verificou o plano atual", "es": "El agente verificó el plan actual", "tr": "Ajan mevcut planı kontrol etti", - "uk": "Агент перевірив поточний план" + "uk": "Агент перевірив поточний план", + "ca": "L'agent ha consultat el pla actual" }, "EXPANDABLE_MESSAGE$SHOW_DETAILS": { "en": "Show details", @@ -8813,7 +9363,8 @@ "pt": "Mostrar detalhes", "es": "Mostrar detalles", "tr": "Detayları göster", - "uk": "Показати деталі" + "uk": "Показати деталі", + "ca": "Mostra els detalls" }, "EXPANDABLE_MESSAGE$HIDE_DETAILS": { "en": "Hide details", @@ -8829,7 +9380,8 @@ "pt": "Ocultar detalhes", "es": "Ocultar detalles", "tr": "Detayları gizle", - "uk": "Приховати деталі" + "uk": "Приховати деталі", + "ca": "Amaga els detalls" }, "AI_SETTINGS$TITLE": { "en": "AI Provider Configuration", @@ -8845,7 +9397,8 @@ "fr": "Configuration du fournisseur d'IA", "tr": "AI Sağlayıcı Yapılandırması", "de": "Einstellungen", - "uk": "Конфігурація постачальника ШІ" + "uk": "Конфігурація постачальника ШІ", + "ca": "Configuració del proveïdor d'IA" }, "SETTINGS$DESCRIPTION": { "en": "To continue, connect an OpenAI, Anthropic, or other LLM account", @@ -8861,7 +9414,8 @@ "fr": "Pour continuer, connectez un compte OpenAI, Anthropic ou autre LLM", "tr": "Devam etmek için bir OpenAI, Anthropic veya başka bir LLM hesabı bağlayın", "de": "Konfigurieren Sie Ihre OpenHands-Umgebung", - "uk": "Щоб продовжити, підключіть обліковий запис OpenAI, Anthropic або інший LLM" + "uk": "Щоб продовжити, підключіть обліковий запис OpenAI, Anthropic або інший LLM", + "ca": "Per continuar, connecteu un compte d'OpenAI, Anthropic o un altre LLM" }, "SETTINGS$WARNING": { "en": "Changing settings during an active session will end the session", @@ -8877,7 +9431,8 @@ "fr": "La modification des paramètres pendant une session active mettra fin à la session", "tr": "Aktif bir oturum sırasında ayarları değiştirmek oturumu sonlandıracaktır", "de": "Einige Einstellungen können nicht geändert werden, während der Agent läuft", - "uk": "Зміна налаштувань під час активного сеансу призведе до його завершення." + "uk": "Зміна налаштувань під час активного сеансу призведе до його завершення.", + "ca": "Canviar la configuració durant una sessió activa finalitzarà la sessió" }, "SIDEBAR$SETTINGS": { "en": "Settings", @@ -8893,7 +9448,8 @@ "fr": "Paramètres", "tr": "Ayarlar", "de": "Einstellungen", - "uk": "Налаштування" + "uk": "Налаштування", + "ca": "Configuració" }, "SIDEBAR$DOCS": { "en": "Documentation", @@ -8909,7 +9465,8 @@ "fr": "Documentation", "tr": "Belgeler", "de": "Dokumentation", - "uk": "Документація" + "uk": "Документація", + "ca": "Documentació" }, "SUGGESTIONS$ADD_DOCS": { "en": "Add best practices docs for contributors", @@ -8925,7 +9482,8 @@ "fr": "Ajouter des documents sur les meilleures pratiques pour les contributeurs", "tr": "Katkıda bulunanlar için en iyi uygulama belgelerini ekle", "de": "Dokumentation hinzufügen", - "uk": "Додайте документи з найкращими практиками для учасників" + "uk": "Додайте документи з найкращими практиками для учасників", + "ca": "Afegeix documentació de bones pràctiques per als col·laboradors" }, "SUGGESTIONS$ADD_DOCKERFILE": { "en": "Add/improve a Dockerfile", @@ -8941,7 +9499,8 @@ "fr": "Ajouter/améliorer un Dockerfile", "tr": "Dockerfile ekle/geliştir", "de": "Dockerfile hinzufügen", - "uk": "Додати/покращити Dockerfile" + "uk": "Додати/покращити Dockerfile", + "ca": "Afegeix/millora un Dockerfile" }, "STATUS$CONNECTED": { "en": "Connected", @@ -8957,7 +9516,8 @@ "ar": "متصل", "fr": "Connecté", "tr": "Bağlandı", - "uk": "Підключено" + "uk": "Підключено", + "ca": "Connectat" }, "STATUS$CONNECTION_LOST": { "en": "Connection lost", @@ -8973,7 +9533,8 @@ "ar": "فُقد الاتصال", "fr": "Connexion perdue", "tr": "Bağlantı kesildi", - "uk": "Втрачено з'єднання" + "uk": "Втрачено з'єднання", + "ca": "Connexió perduda" }, "STATUS$DISCONNECTED_REFRESH_PAGE": { "en": "Disconnected. Please refresh the page", @@ -8989,7 +9550,8 @@ "ar": "تم قطع الاتصال. يرجى تحديث الصفحة", "fr": "Déconnecté. Veuillez actualiser la page", "tr": "Bağlantı kesildi. Lütfen sayfayı yenileyin", - "uk": "Відключено. Будь ласка, оновіть сторінку" + "uk": "Відключено. Будь ласка, оновіть сторінку", + "ca": "Desconnectat. Actualitzeu la pàgina" }, "BROWSER$NO_PAGE_LOADED": { "en": "No page loaded yet. Ask OpenHands to open a URL. Example: \"Open https://example.com\"", @@ -9005,7 +9567,8 @@ "ar": "لم يتم تحميل أي صفحة بعد. اطلب من OpenHands فتح عنوان URL. مثال: \"Open https://example.com\"", "fr": "Aucune page n'a encore été chargée. Demandez à OpenHands d'ouvrir une URL. Exemple : \"Open https://example.com\"", "tr": "Henüz hiçbir sayfa yüklenmedi. OpenHands'e bir URL açmasını isteyin. Örnek: \"Open https://example.com\"", - "uk": "Сторінка ще не завантажена. Попросіть OpenHands відкрити URL-адресу. Приклад: \"Open https://example.com\"" + "uk": "Сторінка ще не завантажена. Попросіть OpenHands відкрити URL-адресу. Приклад: \"Open https://example.com\"", + "ca": "Encara no s'ha carregat cap pàgina. Demaneu a OpenHands que obri una URL. Exemple: \"Obre https://example.com\"" }, "USER$AVATAR_PLACEHOLDER": { "en": "user avatar placeholder", @@ -9021,7 +9584,8 @@ "ar": "عنصر نائب لصورة المستخدم", "no": "plassholder for brukeravatar", "tr": "Kullanıcı avatarı yer tutucusu", - "uk": "заповнювач аватара користувача" + "uk": "заповнювач аватара користувача", + "ca": "marcador de posició de l'avatar de l'usuari" }, "ACCOUNT_SETTINGS$LOGOUT": { "en": "Logout", @@ -9037,7 +9601,8 @@ "fr": "Déconnexion", "tr": "Çıkış yap", "de": "Abmelden", - "uk": "Вийти" + "uk": "Вийти", + "ca": "Tanca la sessió" }, "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL": { "en": "Advanced Options", @@ -9053,7 +9618,8 @@ "it": "Opzioni avanzate", "pt": "Opções avançadas", "tr": "Gelişmiş seçenekler", - "uk": "Розширені параметри" + "uk": "Розширені параметри", + "ca": "Opcions avançades" }, "CONVERSATION$NO_CONVERSATIONS": { "en": "No conversations found", @@ -9069,7 +9635,8 @@ "ar": "لم يتم العثور على محادثات", "no": "Ingen samtaler funnet", "tr": "Konuşma yok", - "uk": "Розмов не знайдено" + "uk": "Розмов не знайдено", + "ca": "No s'han trobat converses" }, "LANDING$SELECT_GIT_REPO": { "en": "Select a Git project", @@ -9085,7 +9652,8 @@ "ar": "اختر مشروع Git", "no": "Velg et Git-prosjekt", "tr": "Depo seç", - "uk": "Виберіть Git-проект" + "uk": "Виберіть Git-проект", + "ca": "Selecciona un projecte Git" }, "BUTTON$SEND": { "en": "Send", @@ -9101,7 +9669,8 @@ "ar": "إرسال", "no": "Send", "tr": "Gönder", - "uk": "Надіслати" + "uk": "Надіслати", + "ca": "Envia" }, "STATUS$BUILDING_RUNTIME": { "en": "Building Runtime...", @@ -9117,7 +9686,8 @@ "fr": "Construction de l'environnement d'exécution...", "tr": "Çalışma ortamı oluşturuluyor...", "de": "Laufzeitumgebung wird erstellt...", - "uk": "Створення середовища виконання..." + "uk": "Створення середовища виконання...", + "ca": "Construint l'entorn d'execució..." }, "SUGGESTIONS$WHAT_TO_BUILD": { "en": "What do you want to build?", @@ -9133,7 +9703,8 @@ "fr": "Que voulez-vous construire ?", "tr": "Ne inşa etmek istiyorsun?", "de": "Was möchten Sie erstellen?", - "uk": "Що ви хочете побудувати?" + "uk": "Що ви хочете побудувати?", + "ca": "Què vols construir?" }, "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": { "en": "Enable Memory Condenser", @@ -9149,7 +9720,8 @@ "it": "Abilita condensatore di memoria", "pt": "Ativar condensador de memória", "es": "Habilitar condensador de memoria", - "tr": "Bellek Yoğunlaştırıcıyı Etkinleştir" + "tr": "Bellek Yoğunlaştırıcıyı Etkinleştir", + "ca": "Activa el condensador de memòria" }, "BUTTON$MARK_HELPFUL": { "en": "Mark this solution as helpful", @@ -9165,7 +9737,8 @@ "fr": "Marquer cette solution comme utile", "tr": "Bu çözümü yararlı olarak işaretle", "ja": "このソリューションが役立つと評価", - "uk": "Позначити це рішення як корисне" + "uk": "Позначити це рішення як корисне", + "ca": "Marca aquesta solució com a útil" }, "BUTTON$MARK_NOT_HELPFUL": { "en": "Mark this solution as not helpful", @@ -9181,7 +9754,8 @@ "fr": "Marquer cette solution comme non utile", "tr": "Bu çözümü yararlı değil olarak işaretle", "ja": "このソリューションが役立たないと評価", - "uk": "Позначити це рішення як некорисне" + "uk": "Позначити це рішення як некорисне", + "ca": "Marca aquesta solució com a no útil" }, "BUTTON$EXPORT_CONVERSATION": { "en": "Export Conversation", @@ -9197,7 +9771,8 @@ "fr": "Exporter la conversation", "tr": "Konuşmayı dışa aktar", "ja": "会話をエクスポート", - "uk": "Експорт розмови" + "uk": "Експорт розмови", + "ca": "Exporta la conversa" }, "BILLING$CLICK_TO_TOP_UP": { "en": "Add funds to Your Account", @@ -9213,7 +9788,8 @@ "fr": "Ajouter des fonds à votre compte", "tr": "Hesabınıza bakiye ekleyin", "de": "Guthaben zu Ihrem Konto hinzufügen", - "uk": "Додайте кошти до свого облікового запису" + "uk": "Додайте кошти до свого облікового запису", + "ca": "Afegiu fons al vostre compte" }, "BILLING$YOUVE_GOT_50": { "en": "You've got $50 in free OpenHands credits", @@ -9229,7 +9805,8 @@ "fr": "Vous avez reçu $50 de crédits OpenHands gratuits", "tr": "OpenHands'de $50 ücretsiz kredi kazandınız", "de": "Sie haben $50 in kostenlosen OpenHands-Guthaben erhalten", - "uk": "Ви отримали 50 доларів у безкоштовних кредитах OpenHands" + "uk": "Ви отримали 50 доларів у безкоштовних кредитах OpenHands", + "ca": "Teniu 50 $ en crèdits gratuïts d'OpenHands" }, "BILLING$ERROR_WHILE_CREATING_SESSION": { "en": "Error occurred while setting up your payment session. Please try again later.", @@ -9245,7 +9822,8 @@ "fr": "Une erreur s'est produite lors de la configuration de votre session de paiement. Veuillez réessayer plus tard.", "tr": "Ödeme oturumunuz kurulurken bir hata oluştu. Lütfen daha sonra tekrar deneyin.", "de": "Beim Einrichten Ihrer Zahlungssitzung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", - "uk": "Під час налаштування сеансу оплати сталася помилка. Будь ласка, спробуйте пізніше." + "uk": "Під час налаштування сеансу оплати сталася помилка. Будь ласка, спробуйте пізніше.", + "ca": "S'ha produït un error en configurar la sessió de pagament. Torneu-ho a intentar més tard." }, "BILLING$CLAIM_YOUR_50": { "en": "Add a credit card with Stripe to claim your $50. We won't charge you without asking first!", @@ -9261,7 +9839,8 @@ "fr": "Ajoutez une carte de crédit avec Stripe pour obtenir 50$. Nous ne vous facturerons pas sans vous demander d'abord !", "tr": "50$ almak için Stripe ile kredi kartı ekleyin. Önce sormadan ücret almayacağız!", "de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. Wir belasten Sie nicht ohne vorherige Zustimmung!", - "uk": "Додайте кредитну картку до Stripe, щоб отримати свої 50 доларів. Ми не стягуватимемо з вас плату без попереднього запиту!" + "uk": "Додайте кредитну картку до Stripe, щоб отримати свої 50 доларів. Ми не стягуватимемо з вас плату без попереднього запиту!", + "ca": "Afegiu una targeta de crèdit amb Stripe per reclamar els vostres 50 $. No us cobrarem sense demanar-vos-ho primer!" }, "BILLING$POWERED_BY": { "en": "Powered by", @@ -9277,7 +9856,8 @@ "fr": "Propulsé par", "tr": "Tarafından desteklenmektedir", "de": "Bereitgestellt von", - "uk": "Працює на базі" + "uk": "Працює на базі", + "ca": "Impulsat per" }, "BILLING$PROCEED_TO_STRIPE": { "en": "Add Billing Info", @@ -9293,7 +9873,8 @@ "fr": "Ajouter les informations de facturation", "tr": "Fatura Bilgisi Ekle", "de": "Zahlungsinformationen hinzufügen", - "uk": "Додати платіжну інформацію" + "uk": "Додати платіжну інформацію", + "ca": "Afegeix informació de facturació" }, "BILLING$YOURE_IN": { "en": "You're in! You can start using your $10 in free credits now.", @@ -9309,7 +9890,8 @@ "fr": "C'est fait ! Vous pouvez commencer à utiliser vos 10 $ de crédits gratuits maintenant.", "tr": "Başardın! Şimdi $10 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.", "de": "Du bist dabei! Du kannst jetzt deine $10 an kostenlosen Guthaben nutzen.", - "uk": "Готово! Ви можете почати використовувати свої безкоштовні кредити на суму 10 доларів США вже зараз." + "uk": "Готово! Ви можете почати використовувати свої безкоштовні кредити на суму 10 доларів США вже зараз.", + "ca": "Ja hi sou! Podeu començar a fer servir els vostres 10 $ en crèdits gratuïts ara." }, "PAYMENT$ADD_FUNDS": { "en": "Add Funds", @@ -9325,7 +9907,8 @@ "fr": "Ajouter des fonds", "tr": "Bakiye Ekle", "de": "Guthaben hinzufügen", - "uk": "Додати кошти" + "uk": "Додати кошти", + "ca": "Afegeix fons" }, "PAYMENT$ADD_CREDIT": { "en": "Add credit", @@ -9341,7 +9924,8 @@ "fr": "Ajouter du crédit", "tr": "Kredi ekle", "de": "Guthaben hinzufügen", - "uk": "Додати кредит" + "uk": "Додати кредит", + "ca": "Afegeix crèdit" }, "PAYMENT$MANAGE_CREDITS": { "en": "Manage Credits", @@ -9357,7 +9941,8 @@ "fr": "Gérer les crédits", "tr": "Kredileri yönet", "de": "Guthaben verwalten", - "uk": "Керування кредитами" + "uk": "Керування кредитами", + "ca": "Gestiona els crèdits" }, "PAYMENT$CANCEL_SUBSCRIPTION": { "en": "Cancel Subscription", @@ -9373,7 +9958,8 @@ "fr": "Annuler l'abonnement", "tr": "Aboneliği iptal et", "de": "Abonnement kündigen", - "uk": "Скасувати підписку" + "uk": "Скасувати підписку", + "ca": "Cancel·la la subscripció" }, "PAYMENT$CANCEL_SUBSCRIPTION_TITLE": { "en": "Cancel Subscription", @@ -9389,7 +9975,8 @@ "fr": "Annuler l'abonnement", "tr": "Aboneliği iptal et", "de": "Abonnement kündigen", - "uk": "Скасувати підписку" + "uk": "Скасувати підписку", + "ca": "Cancel·la la subscripció" }, "PAYMENT$SUBSCRIPTION_CANCELLED": { "en": "Subscription cancelled successfully", @@ -9405,7 +9992,8 @@ "fr": "Abonnement annulé avec succès", "tr": "Abonelik başarıyla iptal edildi", "de": "Abonnement erfolgreich gekündigt", - "uk": "Підписку успішно скасовано" + "uk": "Підписку успішно скасовано", + "ca": "La subscripció s'ha cancel·lat correctament" }, "PAYMENT$NEXT_BILLING_DATE": { "en": "Next billing date: {{date}}", @@ -9421,7 +10009,8 @@ "fr": "Prochaine date de facturation: {{date}}", "tr": "Sonraki fatura tarihi: {{date}}", "de": "Nächstes Abrechnungsdatum: {{date}}", - "uk": "Наступна дата виставлення рахунку: {{date}}" + "uk": "Наступна дата виставлення рахунку: {{date}}", + "ca": "Propera data de facturació: {{date}}" }, "WAITLIST$IF_NOT_JOINED": { "en": "If you haven't already joined the waitlist, please do so to get access.", @@ -9437,7 +10026,8 @@ "fr": "Si vous n'avez pas encore rejoint la liste d'attente, veuillez le faire pour obtenir l'accès.", "tr": "Henüz bekleme listesine katılmadıysanız, erişim elde etmek için lütfen katılın.", "de": "Wenn Sie der Warteliste noch nicht beigetreten sind, tun Sie dies bitte, um Zugang zu erhalten.", - "uk": "Якщо ви ще не записалися до списку очікування, будь ласка, зробіть це, щоб отримати доступ." + "uk": "Якщо ви ще не записалися до списку очікування, будь ласка, зробіть це, щоб отримати доступ.", + "ca": "Si encara no us heu unit a la llista d'espera, feu-ho per obtenir accés." }, "WAITLIST$PATIENCE_MESSAGE": { "en": "Thank you for your patience. We're working hard to give you access as soon as possible.", @@ -9453,7 +10043,8 @@ "fr": "Merci pour votre patience. Nous travaillons dur pour vous donner accès dès que possible.", "tr": "Sabrınız için teşekkür ederiz. Size mümkün olan en kısa sürede erişim sağlamak için çok çalışıyoruz.", "de": "Vielen Dank für Ihre Geduld. Wir arbeiten hart daran, Ihnen so schnell wie möglich Zugang zu gewähren.", - "uk": "Дякуємо за ваше терпіння. Ми наполегливо працюємо, щоб надати вам доступ якомога швидше." + "uk": "Дякуємо за ваше терпіння. Ми наполегливо працюємо, щоб надати вам доступ якомога швидше.", + "ca": "Gràcies per la vostra paciència. Estem treballant dur per donar-vos accés el més aviat possible." }, "WAITLIST$ALMOST_THERE": { "en": "Almost there!", @@ -9469,7 +10060,8 @@ "fr": "Presque là !", "tr": "Neredeyse tamam!", "de": "Fast geschafft!", - "uk": "Майже готово!" + "uk": "Майже готово!", + "ca": "Ja quasi hi sou!" }, "PAYMENT$SUCCESS": { "en": "Payment successful", @@ -9485,7 +10077,8 @@ "fr": "Paiement réussi", "tr": "Ödeme başarılı", "de": "Zahlung erfolgreich", - "uk": "Оплата успішна" + "uk": "Оплата успішна", + "ca": "Pagament realitzat correctament" }, "PAYMENT$CANCELLED": { "en": "Payment cancelled", @@ -9501,7 +10094,8 @@ "fr": "Paiement annulé", "tr": "Ödeme iptal edildi", "de": "Zahlung abgebrochen", - "uk": "Платіж скасовано" + "uk": "Платіж скасовано", + "ca": "Pagament cancel·lat" }, "SUBSCRIPTION$SUCCESS": { "en": "Subscription successful! You now have access to premium features.", @@ -9517,7 +10111,8 @@ "fr": "Abonnement réussi ! Vous avez maintenant accès aux fonctionnalités premium.", "tr": "Abonelik başarılı! Artık premium özelliklere erişiminiz var.", "de": "Abonnement erfolgreich! Sie haben jetzt Zugang zu Premium-Funktionen.", - "uk": "Підписка успішна! Тепер у вас є доступ до преміум-функцій." + "uk": "Підписка успішна! Тепер у вас є доступ до преміум-функцій.", + "ca": "Subscripció realitzada correctament! Ara teniu accés a les funcions premium." }, "SUBSCRIPTION$FAILURE": { "en": "Something went wrong, please try again later.", @@ -9533,7 +10128,8 @@ "fr": "Quelque chose s'est mal passé, veuillez réessayer plus tard.", "tr": "Bir şeyler ters gitti, lütfen daha sonra tekrar deneyin.", "de": "Etwas ist schief gelaufen, bitte versuchen Sie es später erneut.", - "uk": "Щось пішло не так, спробуйте ще раз пізніше." + "uk": "Щось пішло не так, спробуйте ще раз пізніше.", + "ca": "Alguna cosa ha anat malament, torneu-ho a intentar més tard." }, "SERVED_APP$TITLE": { "en": "Served Application", @@ -9549,7 +10145,8 @@ "fr": "Application servie", "tr": "Sunulan Uygulama", "de": "Bereitgestellte Anwendung", - "uk": "Оброблений додаток" + "uk": "Оброблений додаток", + "ca": "Aplicació servida" }, "CONVERSATION$UNKNOWN": { "en": "unknown", @@ -9565,7 +10162,8 @@ "ar": "غير معروف", "fr": "inconnu", "tr": "bilinmeyen", - "uk": "невідомий" + "uk": "невідомий", + "ca": "desconegut" }, "SETTINGS$RUNTIME_OPTION_1X": { "en": "1x (2 core, 8G)", @@ -9581,7 +10179,8 @@ "ar": "1x (2 نواة, 8G)", "fr": "1x (2 cœur, 8G)", "tr": "1x (2 çekirdek, 8G)", - "uk": "1x (2 ядра, 8G)" + "uk": "1x (2 ядра, 8G)", + "ca": "1x (2 nuclis, 8 GB)" }, "SETTINGS$RUNTIME_OPTION_2X": { "en": "2x (4 core, 16G)", @@ -9597,7 +10196,8 @@ "ar": "2x (4 نواة, 16G)", "fr": "2x (4 cœur, 16G)", "tr": "2x (4 çekirdek, 16G)", - "uk": "2x (4 ядра, 16G)" + "uk": "2x (4 ядра, 16G)", + "ca": "2x (4 nuclis, 16 GB)" }, "SETTINGS$GET_IN_TOUCH": { "en": "get in touch for access", @@ -9613,7 +10213,8 @@ "ar": "تواصل معنا للوصول", "fr": "contactez-nous pour l'accès", "tr": "erişim için iletişime geçin", - "uk": "зв'яжіться з нами для отримання доступу" + "uk": "зв'яжіться з нами для отримання доступу", + "ca": "poseu-vos en contacte per obtenir accés" }, "CONVERSATION$NO_METRICS": { "en": "No metrics data available", @@ -9629,7 +10230,8 @@ "ar": "لا توجد بيانات قياس متاحة", "fr": "Aucune donnée métrique disponible", "tr": "Kullanılabilir metrik verisi yok", - "uk": "Немає даних про показники" + "uk": "Немає даних про показники", + "ca": "No hi ha dades de mètriques disponibles" }, "CONVERSATION$DOWNLOAD_ERROR": { "en": "ConversationId unknown, cannot download trajectory", @@ -9645,7 +10247,8 @@ "ar": "معرف المحادثة غير معروف، لا يمكن تنزيل المسار", "fr": "ID de conversation inconnu, impossible de télécharger la trajectoire", "tr": "Konuşma kimliği bilinmiyor, yörünge indirilemiyor", - "uk": "Ідентифікатор розмови невідомий, неможливо завантажити траєкторію" + "uk": "Ідентифікатор розмови невідомий, неможливо завантажити траєкторію", + "ca": "ConversationId desconegut, no es pot descarregar la trajectòria" }, "CONVERSATION$UPDATED": { "en": ", updated", @@ -9661,7 +10264,8 @@ "ar": "، تم التحديث", "fr": ", mis à jour", "tr": ", güncellendi", - "uk": ", оновлено" + "uk": ", оновлено", + "ca": ", actualitzat" }, "CONVERSATION$TOTAL_COST": { "en": "Total Cost", @@ -9677,7 +10281,8 @@ "ar": "التكلفة الإجمالية", "fr": "Coût total ", "tr": "Toplam Maliyet", - "uk": "Загальна вартість" + "uk": "Загальна вартість", + "ca": "Cost total" }, "CONVERSATION$BUDGET": { "en": "Budget", @@ -9693,7 +10298,8 @@ "ar": "الميزانية", "fr": "Budget", "tr": "Bütçe", - "uk": "Бюджет" + "uk": "Бюджет", + "ca": "Pressupost" }, "CONVERSATION$BUDGET_USAGE": { "en": "% used", @@ -9709,7 +10315,8 @@ "ar": "% مستخدم", "fr": "% utilisé", "tr": "% kullanıldı", - "uk": "% використано" + "uk": "% використано", + "ca": "% utilitzat" }, "CONVERSATION$NO_BUDGET_LIMIT": { "en": "No budget limit", @@ -9725,7 +10332,8 @@ "ar": "لا حد للميزانية", "fr": "Pas de limite de budget", "tr": "Bütçe limiti yok", - "uk": "Без обмеження бюджету" + "uk": "Без обмеження бюджету", + "ca": "Sense límit de pressupost" }, "CONVERSATION$INPUT": { "en": "- Input:", @@ -9741,7 +10349,8 @@ "ar": "- المدخلات:", "fr": "- Entrée :", "tr": "- Giriş:", - "uk": "- Вхідні дані:" + "uk": "- Вхідні дані:", + "ca": "- Entrada:" }, "CONVERSATION$OUTPUT": { "en": "- Output:", @@ -9757,7 +10366,8 @@ "ar": "- المخرجات:", "fr": "- Sortie :", "tr": "- Çıkış:", - "uk": "- Результат:" + "uk": "- Результат:", + "ca": "- Sortida:" }, "CONVERSATION$TOTAL": { "en": "- Total:", @@ -9773,7 +10383,8 @@ "ar": "- المجموع:", "fr": "- Total :", "tr": "- Toplam:", - "uk": "- Всього:" + "uk": "- Всього:", + "ca": "- Total:" }, "CONVERSATION$CONTEXT_WINDOW": { "en": "Context Window", @@ -9789,7 +10400,8 @@ "ar": "نافذة السياق", "fr": "Fenêtre de contexte", "tr": "Bağlam Penceresi", - "uk": "Вікно контексту" + "uk": "Вікно контексту", + "ca": "Finestra de context" }, "CONVERSATION$USED": { "en": "used", @@ -9805,7 +10417,8 @@ "ar": "مستخدم", "fr": "utilisé", "tr": "kullanıldı", - "uk": "використано" + "uk": "використано", + "ca": "utilitzat" }, "SETTINGS$RESET_CONFIRMATION": { "en": "Are you sure you want to reset all settings?", @@ -9821,7 +10434,8 @@ "ar": "هل أنت متأكد أنك تريد إعادة تعيين جميع الإعدادات؟", "fr": "Êtes-vous sûr de vouloir réinitialiser tous les paramètres ?", "tr": "Tüm ayarları sıfırlamak istediğinizden emin misiniz?", - "uk": "Ви впевнені, що хочете скинути всі налаштування?" + "uk": "Ви впевнені, що хочете скинути всі налаштування?", + "ca": "Esteu segur que voleu restablir tota la configuració?" }, "ERROR$GENERIC_OOPS": { "en": "Oops! An error occurred!", @@ -9837,7 +10451,8 @@ "ar": "عفوا! حدث خطأ!", "fr": "Oups ! Une erreur s'est produite !", "tr": "Hay aksi! Bir hata oluştu!", - "uk": "Ой! Сталася помилка!" + "uk": "Ой! Сталася помилка!", + "ca": "Vaja! S'ha produït un error!" }, "ERROR$UNKNOWN": { "en": "Uh oh, an unknown error occurred!", @@ -9853,7 +10468,8 @@ "ar": "أوه، حدث خطأ غير معروف!", "fr": "Oh non, une erreur inconnue s'est produite !", "tr": "Hay aksi, bilinmeyen bir hata oluştu!", - "uk": "Ой, сталася невідома помилка!" + "uk": "Ой, сталася невідома помилка!", + "ca": "Vaja, s'ha produït un error desconegut!" }, "SETTINGS$FOR_OTHER_OPTIONS": { "en": "For other options", @@ -9869,7 +10485,8 @@ "ar": "للخيارات الأخرى،", "fr": "Pour d'autres options", "tr": "Diğer seçenekler için", - "uk": "Щодо інших варіантів" + "uk": "Щодо інших варіантів", + "ca": "Per a altres opcions" }, "SETTINGS$SEE_ADVANCED_SETTINGS": { "en": "see advanced settings", @@ -9885,7 +10502,8 @@ "ar": "انظر الإعدادات المتقدمة", "fr": "voir les paramètres avancés", "tr": "gelişmiş ayarlara bakın", - "uk": "перегляньте розширені налаштування" + "uk": "перегляньте розширені налаштування", + "ca": "consulteu la configuració avançada" }, "SETTINGS_FORM$API_KEY": { "en": "API Key", @@ -9901,7 +10519,8 @@ "ar": "مفتاح API", "fr": "Clé API", "tr": "API Anahtarı", - "uk": "API ключ" + "uk": "API ключ", + "ca": "Clau d'API" }, "SETTINGS_FORM$BASE_URL": { "en": "Base URL", @@ -9917,7 +10536,8 @@ "ar": "عنوان URL الأساسي", "fr": "URL de base", "tr": "Temel URL", - "uk": "Базовий URL" + "uk": "Базовий URL", + "ca": "URL base" }, "GITHUB$CONNECT_TO_GITHUB": { "en": "Log in with GitHub", @@ -9933,7 +10553,8 @@ "ar": "الاتصال بـ GitHub", "fr": "Se connecter à GitHub", "tr": "GitHub'a bağlan", - "uk": "Увійти за допомогою GitHub" + "uk": "Увійти за допомогою GitHub", + "ca": "Inicia sessió amb GitHub" }, "GITLAB$CONNECT_TO_GITLAB": { "en": "Log in with GitLab", @@ -9949,7 +10570,8 @@ "ar": "الاتصال بـ GitLab", "fr": "Se connecter à GitLab", "tr": "GitLab'a bağlan", - "uk": "Увійти за допомогою GitLab" + "uk": "Увійти за допомогою GitLab", + "ca": "Inicia sessió amb GitLab" }, "BITBUCKET$CONNECT_TO_BITBUCKET": { "en": "Log in with Bitbucket", @@ -9965,7 +10587,8 @@ "ar": "الاتصال بـ Bitbucket", "fr": "Se connecter à Bitbucket", "tr": "Bitbucket'a bağlan", - "uk": "Увійти за допомогою Bitbucket" + "uk": "Увійти за допомогою Bitbucket", + "ca": "Inicia sessió amb Bitbucket" }, "BITBUCKET_DATA_CENTER$CONNECT_TO_BITBUCKET_DATA_CENTER": { "en": "Log in with Bitbucket Data Center", @@ -9981,7 +10604,8 @@ "ar": "الاتصال بـ Bitbucket Data Center", "fr": "Se connecter à Bitbucket Data Center", "tr": "Bitbucket Data Center'a bağlan", - "uk": "Увійти за допомогою Bitbucket Data Center" + "uk": "Увійти за допомогою Bitbucket Data Center", + "ca": "Inicia sessió amb Bitbucket Data Center" }, "ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO": { "en": "Login with Enterprise SSO", @@ -9997,7 +10621,8 @@ "ar": "تسجيل الدخول باستخدام Enterprise SSO", "fr": "Se connecter avec Enterprise SSO", "tr": "Enterprise SSO ile giriş yap", - "uk": "Увійти за допомогою Enterprise SSO" + "uk": "Увійти за допомогою Enterprise SSO", + "ca": "Inicia sessió amb SSO empresarial" }, "AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER": { "en": "Log in to OpenHands", @@ -10013,7 +10638,8 @@ "fr": "Connectez-vous avec votre fournisseur d'identité", "tr": "Kimlik sağlayıcınızla giriş yapın", "de": "Melden Sie sich mit Ihrem Identitätsanbieter an", - "uk": "Увійти до OpenHands" + "uk": "Увійти до OpenHands", + "ca": "Inicia sessió a OpenHands" }, "WAITLIST$JOIN_WAITLIST": { "en": "Join Waitlist", @@ -10029,7 +10655,8 @@ "ar": "الانضمام إلى قائمة الانتظار", "fr": "Rejoindre la liste d'attente", "tr": "Bekleme listesine katıl", - "uk": "Приєднатися до списку очікування" + "uk": "Приєднатися до списку очікування", + "ca": "Uneix-te a la llista d'espera" }, "CONVERSATION$DELETE_WARNING": { "en": "Are you sure you want to delete this conversation? This action cannot be undone.", @@ -10045,7 +10672,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer cette conversation ? Cette action ne peut pas être annulée.", "tr": "Bu konuşmayı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "de": "Sind Sie sicher, dass Sie dieses Gespräch löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "uk": "Ви впевнені, що хочете видалити цю розмову? Цю дію не можна скасувати." + "uk": "Ви впевнені, що хочете видалити цю розмову? Цю дію не можна скасувати.", + "ca": "Esteu segur que voleu eliminar aquesta conversa? Aquesta acció no es pot desfer." }, "CONVERSATION$DELETE_WARNING_WITH_TITLE": { "en": "Are you sure you want to delete the \"{{title}}\" conversation? This action cannot be undone.", @@ -10061,7 +10689,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer la conversation « {{title}} » ? Cette action ne peut pas être annulée.", "tr": "\"{{title}}\" konuşmasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "de": "Sind Sie sicher, dass Sie das Gespräch „{{title}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "uk": "Ви впевнені, що хочете видалити розмову «{{title}}»? Цю дію не можна скасувати." + "uk": "Ви впевнені, що хочете видалити розмову «{{title}}»? Цю дію не можна скасувати.", + "ca": "Esteu segur que voleu eliminar la conversa \"{{title}}\"? Aquesta acció no es pot desfer." }, "FEEDBACK$TITLE": { "en": "Feedback", @@ -10077,7 +10706,8 @@ "fr": "Retour d'information", "tr": "Geri bildirim", "de": "Feedback", - "uk": "Зворотній зв'язок" + "uk": "Зворотній зв'язок", + "ca": "Comentaris" }, "FEEDBACK$DESCRIPTION": { "en": "We value your feedback. Please share your thoughts with us.", @@ -10093,7 +10723,8 @@ "fr": "Nous apprécions vos commentaires. Veuillez partager vos réflexions avec nous.", "tr": "Geri bildiriminizi değerlendiriyoruz. Lütfen düşüncelerinizi bizimle paylaşın.", "de": "Wir schätzen Ihr Feedback. Bitte teilen Sie uns Ihre Gedanken mit.", - "uk": "Ми цінуємо ваш відгук. Будь ласка, поділіться з нами своїми думками." + "uk": "Ми цінуємо ваш відгук. Будь ласка, поділіться з нами своїми думками.", + "ca": "Valorem els vostres comentaris. Compartiu les vostres opinions amb nosaltres." }, "EXIT_PROJECT$WARNING": { "en": "Are you sure you want to exit this project? Any unsaved changes will be lost.", @@ -10109,7 +10740,8 @@ "fr": "Êtes-vous sûr de vouloir quitter ce projet ? Toutes les modifications non enregistrées seront perdues.", "tr": "Bu projeden çıkmak istediğinizden emin misiniz? Kaydedilmemiş değişiklikler kaybolacaktır.", "de": "Sind Sie sicher, dass Sie dieses Projekt beenden möchten? Alle nicht gespeicherten Änderungen gehen verloren.", - "uk": "Ви впевнені, що хочете вийти з цього проєкту? Усі незбережені зміни буде втрачено." + "uk": "Ви впевнені, що хочете вийти з цього проєкту? Усі незбережені зміни буде втрачено.", + "ca": "Esteu segur que voleu sortir d'aquest projecte? Es perdran els canvis no desats." }, "MODEL_SELECTOR$VERIFIED": { "en": "Verified Models", @@ -10125,7 +10757,8 @@ "fr": "Modèles vérifiés", "tr": "Doğrulanmış Modeller", "de": "Verifizierte Modelle", - "uk": "Перевірені моделі" + "uk": "Перевірені моделі", + "ca": "Models verificats" }, "MODEL_SELECTOR$OTHERS": { "en": "Other Models", @@ -10141,7 +10774,8 @@ "fr": "Autres modèles", "tr": "Diğer Modeller", "de": "Andere Modelle", - "uk": "Інші моделі" + "uk": "Інші моделі", + "ca": "Altres models" }, "GITLAB$TOKEN_LABEL": { "en": "GitLab Token", @@ -10157,7 +10791,8 @@ "fr": "Jeton GitLab", "tr": "GitLab Jetonu", "de": "GitLab-Token", - "uk": "GitLab токен" + "uk": "GitLab токен", + "ca": "Token de GitLab" }, "GITLAB$HOST_LABEL": { "en": "GitLab Host (optional)", @@ -10173,7 +10808,8 @@ "fr": "Hôte GitLab (optionnel)", "tr": "GitLab Sunucusu (isteğe bağlı)", "de": "GitLab-Host (optional)", - "uk": "Хост GitLab (необов'язково)" + "uk": "Хост GitLab (необов'язково)", + "ca": "Servidor de GitLab (opcional)" }, "GITLAB$GET_TOKEN": { "en": "Generate a token on", @@ -10189,7 +10825,8 @@ "fr": "Générer un jeton sur", "tr": "Üzerinde bir jeton oluştur", "de": "Token generieren auf", - "uk": "Згенерувати токен на" + "uk": "Згенерувати токен на", + "ca": "Genera un token a" }, "GITLAB$TOKEN_HELP_TEXT": { "en": "Get your <0>GitLab token or <1>click here for instructions", @@ -10205,7 +10842,8 @@ "fr": "Obtenez votre <0>jeton GitLab ou <1>cliquez ici pour les instructions", "tr": "<0>GitLab jetonu alın veya <1>talimatlar için buraya tıklayın", "de": "Holen Sie sich Ihren <0>GitLab-Token oder <1>klicken Sie hier für Anweisungen", - "uk": "Get your <0>GitLab token or <1>click here for instructions" + "uk": "Get your <0>GitLab token or <1>click here for instructions", + "ca": "Obteniu el vostre <0>token de GitLab o <1>feu clic aquí per obtenir instruccions" }, "GITLAB$TOKEN_LINK_TEXT": { "en": "GitLab token", @@ -10221,7 +10859,8 @@ "fr": "jeton GitLab", "tr": "GitLab jetonu", "de": "GitLab-Token", - "uk": "GitLab токен" + "uk": "GitLab токен", + "ca": "token de GitLab" }, "GITLAB$INSTRUCTIONS_LINK_TEXT": { "en": "click here for instructions", @@ -10237,7 +10876,8 @@ "fr": "cliquez ici pour les instructions", "tr": "talimatlar için buraya tıklayın", "de": "klicken Sie hier für Anweisungen", - "uk": "натисніть тут, щоб отримати інструкції" + "uk": "натисніть тут, щоб отримати інструкції", + "ca": "feu clic aquí per obtenir instruccions" }, "GITLAB$WEBHOOK_MANAGER_TITLE": { "en": "Webhook Management", @@ -10253,7 +10893,8 @@ "fr": "Gestion des Webhooks", "tr": "Webhook Yönetimi", "de": "Webhook-Verwaltung", - "uk": "Керування Webhook" + "uk": "Керування Webhook", + "ca": "Gestió de Webhooks" }, "GITLAB$WEBHOOK_MANAGER_DESCRIPTION": { "en": "Manage webhooks for your GitLab projects and groups. Webhooks enable OpenHands to receive notifications from GitLab. Note: If a webhook is already installed, you must first delete it through the GitLab UI before reinstalling.", @@ -10269,7 +10910,8 @@ "fr": "Gérez les webhooks pour vos projets et groupes GitLab. Les webhooks permettent à OpenHands de recevoir des notifications de GitLab. Remarque : Si un webhook est déjà installé, vous devez d'abord le supprimer via l'interface GitLab avant de le réinstaller.", "tr": "GitLab projeleriniz ve gruplarınız için webhook'ları yönetin. Webhook'lar OpenHands'in GitLab'dan bildirim almasını sağlar. Not: Bir webhook zaten yüklüyse, yeniden yüklemeden önce GitLab arayüzü üzerinden silmeniz gerekir.", "de": "Verwalten Sie Webhooks für Ihre GitLab-Projekte und -Gruppen. Webhooks ermöglichen es OpenHands, Benachrichtigungen von GitLab zu empfangen. Hinweis: Wenn ein Webhook bereits installiert ist, müssen Sie ihn zuerst über die GitLab-Benutzeroberfläche löschen, bevor Sie ihn neu installieren.", - "uk": "Керуйте вебхуками для ваших проектів та груп GitLab. Вебхуки дозволяють OpenHands отримувати сповіщення від GitLab. Примітка: Якщо вебхук вже встановлено, ви повинні спочатку видалити його через інтерфейс GitLab перед повторним встановленням." + "uk": "Керуйте вебхуками для ваших проектів та груп GitLab. Вебхуки дозволяють OpenHands отримувати сповіщення від GitLab. Примітка: Якщо вебхук вже встановлено, ви повинні спочатку видалити його через інтерфейс GitLab перед повторним встановленням.", + "ca": "Gestioneu els webhooks per als vostres projectes i grups de GitLab. Els webhooks permeten a OpenHands rebre notificacions de GitLab. Nota: Si ja hi ha un webhook instal·lat, primer l'heu d'eliminar des de la interfície de GitLab abans de reinstal·lar-lo." }, "GITLAB$WEBHOOK_MANAGER_LOADING": { "en": "Loading resources...", @@ -10285,7 +10927,8 @@ "fr": "Chargement des ressources...", "tr": "Kaynaklar yükleniyor...", "de": "Ressourcen werden geladen...", - "uk": "Завантаження ресурсів..." + "uk": "Завантаження ресурсів...", + "ca": "Carregant recursos..." }, "GITLAB$WEBHOOK_MANAGER_ERROR": { "en": "Failed to load resources. Please try again.", @@ -10301,7 +10944,8 @@ "fr": "Échec du chargement des ressources. Veuillez réessayer.", "tr": "Kaynaklar yüklenemedi. Lütfen tekrar deneyin.", "de": "Ressourcen konnten nicht geladen werden. Bitte versuchen Sie es erneut.", - "uk": "Не вдалося завантажити ресурси. Будь ласка, спробуйте ще раз." + "uk": "Не вдалося завантажити ресурси. Будь ласка, спробуйте ще раз.", + "ca": "No s'han pogut carregar els recursos. Torneu-ho a intentar." }, "GITLAB$WEBHOOK_MANAGER_NO_RESOURCES": { "en": "No projects or groups found where you have admin access.", @@ -10317,7 +10961,8 @@ "fr": "Aucun projet ou groupe trouvé où vous avez un accès administrateur.", "tr": "Yönetici erişiminizin olduğu proje veya grup bulunamadı.", "de": "Keine Projekte oder Gruppen gefunden, auf die Sie Administratorzugriff haben.", - "uk": "Не знайдено проектів або груп, де ви маєте адміністраторський доступ." + "uk": "Не знайдено проектів або груп, де ви маєте адміністраторський доступ.", + "ca": "No s'han trobat projectes ni grups on tingueu accés d'administrador." }, "GITLAB$WEBHOOK_REINSTALL": { "en": "Reinstall", @@ -10333,7 +10978,8 @@ "fr": "Réinstaller", "tr": "Yeniden Yükle", "de": "Neu installieren", - "uk": "Перевстановити" + "uk": "Перевстановити", + "ca": "Reinstal·la" }, "GITLAB$WEBHOOK_REINSTALLING": { "en": "Reinstalling...", @@ -10349,7 +10995,8 @@ "fr": "Réinstallation...", "tr": "Yeniden yükleniyor...", "de": "Wird neu installiert...", - "uk": "Перевстановлення..." + "uk": "Перевстановлення...", + "ca": "Reinstal·lant..." }, "GITLAB$WEBHOOK_REINSTALL_SUCCESS": { "en": "Webhook reinstalled successfully", @@ -10365,7 +11012,8 @@ "fr": "Webhook réinstallé avec succès", "tr": "Webhook başarıyla yeniden yüklendi", "de": "Webhook erfolgreich neu installiert", - "uk": "Вебхук успішно перевстановлено" + "uk": "Вебхук успішно перевстановлено", + "ca": "Webhook reinstal·lat correctament" }, "GITLAB$WEBHOOK_COLUMN_RESOURCE": { "en": "Resource", @@ -10381,7 +11029,8 @@ "fr": "Ressource", "tr": "Kaynak", "de": "Ressource", - "uk": "Ресурс" + "uk": "Ресурс", + "ca": "Recurs" }, "GITLAB$WEBHOOK_COLUMN_TYPE": { "en": "Type", @@ -10397,7 +11046,8 @@ "fr": "Type", "tr": "Tür", "de": "Typ", - "uk": "Тип" + "uk": "Тип", + "ca": "Tipus" }, "GITLAB$WEBHOOK_COLUMN_STATUS": { "en": "Status", @@ -10413,7 +11063,8 @@ "fr": "Statut", "tr": "Durum", "de": "Status", - "uk": "Статус" + "uk": "Статус", + "ca": "Estat" }, "GITLAB$WEBHOOK_COLUMN_ACTION": { "en": "Action", @@ -10429,7 +11080,8 @@ "fr": "Action", "tr": "Eylem", "de": "Aktion", - "uk": "Дія" + "uk": "Дія", + "ca": "Acció" }, "GITLAB$WEBHOOK_STATUS_INSTALLED": { "en": "Installed", @@ -10445,7 +11097,8 @@ "fr": "Installé", "tr": "Yüklü", "de": "Installiert", - "uk": "Встановлено" + "uk": "Встановлено", + "ca": "Instal·lat" }, "GITLAB$WEBHOOK_STATUS_NOT_INSTALLED": { "en": "Not Installed", @@ -10461,7 +11114,8 @@ "fr": "Non installé", "tr": "Yüklü değil", "de": "Nicht installiert", - "uk": "Не встановлено" + "uk": "Не встановлено", + "ca": "No instal·lat" }, "GITLAB$WEBHOOK_STATUS_FAILED": { "en": "Failed", @@ -10477,7 +11131,8 @@ "fr": "Échoué", "tr": "Başarısız", "de": "Fehlgeschlagen", - "uk": "Помилка" + "uk": "Помилка", + "ca": "Error" }, "GITLAB$WEBHOOK_REINSTALL_FAILED": { "en": "Failed to reinstall webhook", @@ -10493,7 +11148,8 @@ "fr": "Échec de la réinstallation du webhook", "tr": "Webhook yeniden yüklenemedi", "de": "Webhook konnte nicht neu installiert werden", - "uk": "Не вдалося перевстановити вебхук" + "uk": "Не вдалося перевстановити вебхук", + "ca": "No s'ha pogut reinstal·lar el webhook" }, "BITBUCKET$TOKEN_LABEL": { "en": "Bitbucket Token", @@ -10509,7 +11165,8 @@ "fr": "Jeton Bitbucket", "tr": "Bitbucket Token", "de": "Bitbucket-Token", - "uk": "Токен Bitbucket" + "uk": "Токен Bitbucket", + "ca": "Token de Bitbucket" }, "BITBUCKET$HOST_LABEL": { "en": "Bitbucket Host", @@ -10525,7 +11182,8 @@ "fr": "Hôte Bitbucket", "tr": "Bitbucket Sunucu", "de": "Bitbucket-Host", - "uk": "Хост Bitbucket" + "uk": "Хост Bitbucket", + "ca": "Servidor de Bitbucket" }, "BITBUCKET$GET_TOKEN": { "en": "Get a Bitbucket token", @@ -10541,7 +11199,8 @@ "fr": "Obtenir un jeton Bitbucket", "tr": "Bitbucket token al", "de": "Bitbucket-Token erhalten", - "uk": "Отримати токен Bitbucket" + "uk": "Отримати токен Bitbucket", + "ca": "Obteniu un token de Bitbucket" }, "BITBUCKET$TOKEN_HELP_TEXT": { "en": "Get your <0>Bitbucket app password or <1>click here for instructions. Enter it in the format 'username:app_password'.", @@ -10557,7 +11216,8 @@ "fr": "Obtenez votre <0>mot de passe d'application Bitbucket ou <1>cliquez ici pour les instructions. Saisissez-le au format 'nom d'utilisateur:mot de passe d'application'.", "tr": "<0>Bitbucket uygulama şifrenizi alın veya <1>talimatlar için buraya tıklayın. 'kullanıcı adı:uygulama şifresi' formatında girin.", "de": "Holen Sie sich Ihr <0>Bitbucket App-Passwort oder <1>klicken Sie hier für Anweisungen. Geben Sie es im Format 'Benutzername:App-Passwort' ein.", - "uk": "Отримайте свій <0>пароль додатка Bitbucket або <1>натисніть тут, щоб отримати інструкції. Введіть його у форматі 'ім'я користувача:пароль додатка'." + "uk": "Отримайте свій <0>пароль додатка Bitbucket або <1>натисніть тут, щоб отримати інструкції. Введіть його у форматі 'ім'я користувача:пароль додатка'.", + "ca": "Obteniu la vostra <0>contrasenya d'aplicació de Bitbucket o <1>feu clic aquí per obtenir instruccions. Introduïu-la en el format 'username:app_password'." }, "BITBUCKET$TOKEN_LINK_TEXT": { "en": "Bitbucket app password", @@ -10573,7 +11233,8 @@ "fr": "mot de passe d'application Bitbucket", "tr": "Bitbucket uygulama şifresi", "de": "Bitbucket App-Passwort", - "uk": "пароль додатка Bitbucket" + "uk": "пароль додатка Bitbucket", + "ca": "contrasenya d'aplicació de Bitbucket" }, "BITBUCKET$INSTRUCTIONS_LINK_TEXT": { "en": "click here for instructions", @@ -10589,7 +11250,8 @@ "fr": "cliquez ici pour les instructions", "tr": "talimatlar için buraya tıklayın", "de": "klicken Sie hier für Anweisungen", - "uk": "натисніть тут, щоб отримати інструкції" + "uk": "натисніть тут, щоб отримати інструкції", + "ca": "feu clic aquí per obtenir instruccions" }, "BITBUCKET_DATA_CENTER$TOKEN_LABEL": { "en": "Bitbucket Data Center Token", @@ -10605,7 +11267,8 @@ "pt": "Token do Bitbucket Data Center", "es": "Token de Bitbucket Data Center", "tr": "Bitbucket Data Center Token", - "uk": "Токен Bitbucket Data Center" + "uk": "Токен Bitbucket Data Center", + "ca": "Token de Bitbucket Data Center" }, "BITBUCKET_DATA_CENTER$HOST_LABEL": { "en": "Bitbucket Data Center Host", @@ -10621,7 +11284,8 @@ "pt": "Host do Bitbucket Data Center", "es": "Host de Bitbucket Data Center", "tr": "Bitbucket Data Center Sunucu", - "uk": "Хост Bitbucket Data Center" + "uk": "Хост Bitbucket Data Center", + "ca": "Servidor de Bitbucket Data Center" }, "BITBUCKET_DATA_CENTER$TOKEN_HELP_TEXT": { "en": "Create an <0>HTTP access token in your Bitbucket Data Center instance with repository read/write and pull request read/write permissions. For personal access tokens, use the format 'username:token'. For project tokens, use the format 'x-token-auth:your-token'.", @@ -10637,7 +11301,8 @@ "pt": "Crie um <0>token de acesso HTTP na sua instância do Bitbucket Data Center com permissões de leitura/gravação de repositório e pull request. Para tokens de acesso pessoal, use o formato 'username:token'. Para tokens de projeto, use o formato 'x-token-auth:your-token'.", "es": "Cree un <0>token de acceso HTTP en su instancia de Bitbucket Data Center con permisos de lectura/escritura de repositorio y pull request. Para los tokens de acceso personal, use el formato 'username:token'. Para los tokens de proyecto, use el formato 'x-token-auth:your-token'.", "tr": "Bitbucket Data Center örneğinizde depo okuma/yazma ve pull request okuma/yazma izinlerine sahip bir <0>HTTP erişim jetonu oluşturun. Kişisel erişim jetonları için 'username:token' biçimini kullanın. Proje jetonları için 'x-token-auth:your-token' biçimini kullanın.", - "uk": "Створіть <0>HTTP-токен доступу у вашому екземплярі Bitbucket Data Center з правами читання/запису репозиторію та pull request. Для особистих токенів доступу використовуйте формат 'username:token'. Для токенів проекту використовуйте формат 'x-token-auth:your-token'." + "uk": "Створіть <0>HTTP-токен доступу у вашому екземплярі Bitbucket Data Center з правами читання/запису репозиторію та pull request. Для особистих токенів доступу використовуйте формат 'username:token'. Для токенів проекту використовуйте формат 'x-token-auth:your-token'.", + "ca": "Creeu un <0>token d'accés HTTP a la vostra instància de Bitbucket Data Center amb permisos de lectura/escriptura de repositori i lectura/escriptura de sol·licitud de canvis. Per als tokens d'accés personal, feu servir el format 'username:token'. Per als tokens de projecte, feu servir el format 'x-token-auth:your-token'." }, "GITLAB$OR_SEE": { "en": "or see the", @@ -10653,7 +11318,8 @@ "fr": "ou voir la", "tr": "veya bak", "de": "oder siehe", - "uk": "або перегляньте" + "uk": "або перегляньте", + "ca": "o consulteu el" }, "AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED_STOPPED": { "en": "Pause button pressed. Agent is stopped. The action has not been executed.", @@ -10669,7 +11335,8 @@ "fr": "Bouton pause enfoncé. L'agent est arrêté. L'action n'a pas été exécutée.", "tr": "Duraklat düğmesine basıldı. Ajan durduruldu. Eylem gerçekleştirilmedi.", "de": "Pausentaste gedrückt. Agent ist gestoppt. Die Aktion wurde nicht ausgeführt.", - "uk": "Натиснуто кнопку паузи. Агент зупинений. Дію не виконано." + "uk": "Натиснуто кнопку паузи. Агент зупинений. Дію не виконано.", + "ca": "S'ha premut el botó de pausa. L'agent s'ha aturat. L'acció no s'ha executat." }, "AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED_ERROR": { "en": "The action has not been executed due to a runtime error. The runtime system may have crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.", @@ -10685,7 +11352,8 @@ "fr": "L'action n'a pas été exécutée en raison d'une erreur d'exécution. Le système d'exécution peut s'être planté et avoir redémarré en raison de contraintes de ressources. Tout état du système, dépendances ou variables d'environnement précédemment établis peuvent avoir été perdus.", "tr": "Çalışma zamanı hatası nedeniyle eylem yürütülmedi. Çalışma zamanı sistemi kaynak kısıtlamaları nedeniyle çökmüş ve yeniden başlamış olabilir. Daha önce kurulmuş olan herhangi bir sistem durumu, bağımlılıklar veya ortam değişkenleri kaybolmuş olabilir.", "de": "Die Aktion wurde aufgrund eines Laufzeitfehlers nicht ausgeführt. Das Laufzeitsystem ist möglicherweise aufgrund von Ressourcenbeschränkungen abgestürzt und neu gestartet worden. Alle zuvor eingerichteten Systemzustände, Abhängigkeiten oder Umgebungsvariablen sind möglicherweise verloren gegangen.", - "uk": "Дію не виконано через помилку виконання. Система виконання могла зазнати збою та перезапуститися через обмеження ресурсів. Можливо, було втрачено будь-який раніше встановлений стан системи, залежності або змінні середовища." + "uk": "Дію не виконано через помилку виконання. Система виконання могла зазнати збою та перезапуститися через обмеження ресурсів. Можливо, було втрачено будь-який раніше встановлений стан системи, залежності або змінні середовища.", + "ca": "L'acció no s'ha executat a causa d'un error de l'entorn d'execució. El sistema de l'entorn d'execució pot haver fallat i reiniciat a causa de restriccions de recursos. Qualsevol estat del sistema, dependències o variables d'entorn establerts anteriorment poden haver-se perdut." }, "DIFF_VIEWER$LOADING": { "en": "Loading changes...", @@ -10701,7 +11369,8 @@ "fr": "Chargement des modifications...", "tr": "Değişiklikler yükleniyor...", "de": "Änderungen werden geladen...", - "uk": "Завантаження змін..." + "uk": "Завантаження змін...", + "ca": "Carregant els canvis..." }, "DIFF_VIEWER$GETTING_LATEST_CHANGES": { "en": "Getting latest changes...", @@ -10717,7 +11386,8 @@ "fr": "Obtention des dernières modifications...", "tr": "Son değişiklikleri alıyor...", "de": "Aktuellste Änderungen abrufen...", - "uk": "Отримання останніх змін..." + "uk": "Отримання останніх змін...", + "ca": "Obtenint els darrers canvis..." }, "DIFF_VIEWER$NOT_A_GIT_REPO": { "en": "Your current workspace is not a git repository.", @@ -10733,7 +11403,8 @@ "fr": "Votre espace de travail actuel n'est pas un dépôt git.", "tr": "Mevcut çalışma alanınız bir git deposu değil.", "de": "Ihr aktueller Arbeitsbereich ist kein git-Repository.", - "uk": "Ваша поточна робоча область не є репозиторієм git." + "uk": "Ваша поточна робоча область не є репозиторієм git.", + "ca": "L'espai de treball actual no és un repositori git." }, "DIFF_VIEWER$ASK_OH": { "en": "Ask OpenHands to initialize a git repo to activate this UI.", @@ -10749,7 +11420,8 @@ "fr": "Demandez à OpenHands d'initialiser un dépôt git pour activer cette interface utilisateur.", "tr": "Bu UI'yi etkinleştirmek için OpenHands'tan bir git deposunu başlatmasını isteyin.", "de": "Bitten Sie OpenHands, ein git-Repository zu initialisieren, um diese Benutzeroberfläche zu aktivieren.", - "uk": "Попросіть OpenHands ініціалізувати git-репозиторій, щоб активувати цей інтерфейс користувача." + "uk": "Попросіть OpenHands ініціалізувати git-репозиторій, щоб активувати цей інтерфейс користувача.", + "ca": "Demaneu a OpenHands que inicialitzi un repositori git per activar aquesta interfície." }, "DIFF_VIEWER$NO_CHANGES": { "en": "OpenHands hasn't made any changes yet", @@ -10765,7 +11437,8 @@ "fr": "OpenHands n'a pas encore apporté de modifications", "tr": "OpenHands henüz herhangi bir değişiklik yapmadı", "de": "OpenHands hat noch keine Änderungen vorgenommen", - "uk": "OpenHands ще не вніс жодних змін" + "uk": "OpenHands ще не вніс жодних змін", + "ca": "L'OpenHands encara no ha fet cap canvi" }, "DIFF_VIEWER$WAITING_FOR_RUNTIME": { "en": "Waiting for runtime to start...", @@ -10781,7 +11454,8 @@ "fr": "En attente du démarrage de l'exécution...", "tr": "Çalışma zamanının başlamasını bekliyor...", "de": "Warten auf den Start der Laufzeit...", - "uk": "Очікування початку виконання..." + "uk": "Очікування початку виконання...", + "ca": "Esperant que l'entorn d'execució s'iniciï..." }, "SYSTEM_MESSAGE_MODAL$TITLE": { "en": "Agent Tools & Metadata", @@ -10797,7 +11471,8 @@ "pt": "Ferramentas e metadados do agente", "es": "Herramientas y metadatos del agente", "tr": "Ajan Araçları ve Meta Verileri", - "uk": "Інструменти та метадані агента" + "uk": "Інструменти та метадані агента", + "ca": "Eines i metadades de l'agent" }, "SYSTEM_MESSAGE_MODAL$AGENT_CLASS": { "en": "Agent Class:", @@ -10813,7 +11488,8 @@ "pt": "Classe do agente:", "es": "Clase de agente:", "tr": "Ajan Sınıfı:", - "uk": "Клас агента:" + "uk": "Клас агента:", + "ca": "Classe de l'agent:" }, "SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION": { "en": "OpenHands Version:", @@ -10829,7 +11505,8 @@ "pt": "Versão OpenHands:", "es": "Versión de OpenHands:", "tr": "OpenHands Sürümü:", - "uk": "OpenHands версія:" + "uk": "OpenHands версія:", + "ca": "Versió d'OpenHands:" }, "SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB": { "en": "System Message", @@ -10845,7 +11522,8 @@ "pt": "Mensagem do sistema", "es": "Mensaje del sistema", "tr": "Sistem Mesajı", - "uk": "Системне повідомлення" + "uk": "Системне повідомлення", + "ca": "Missatge del sistema" }, "SYSTEM_MESSAGE_MODAL$TOOLS_TAB": { "en": "Available Tools", @@ -10861,7 +11539,8 @@ "pt": "Ferramentas disponíveis", "es": "Herramientas disponibles", "tr": "Kullanılabilir Araçlar", - "uk": "Доступні інструменти" + "uk": "Доступні інструменти", + "ca": "Eines disponibles" }, "SYSTEM_MESSAGE_MODAL$PARAMETERS": { "en": "Parameters:", @@ -10877,7 +11556,8 @@ "pt": "Parâmetros:", "es": "Parámetros:", "tr": "Parametreler:", - "uk": "Параметри:" + "uk": "Параметри:", + "ca": "Paràmetres:" }, "SYSTEM_MESSAGE_MODAL$NO_TOOLS": { "en": "No tools available for this agent", @@ -10893,7 +11573,8 @@ "pt": "Nenhuma ferramenta disponível para este agente", "es": "No hay herramientas disponibles para este agente", "tr": "Bu ajan için kullanılabilir araç yok", - "uk": "Для цього агента немає доступних інструментів" + "uk": "Для цього агента немає доступних інструментів", + "ca": "No hi ha eines disponibles per a aquest agent" }, "TOS$ACCEPT_TERMS_OF_SERVICE": { "en": "Accept Terms of Service", @@ -10909,7 +11590,8 @@ "uk": "Прийняти Умови надання послуг", "no": "Godta vilkår for tjenesten", "ar": "قبول شروط الخدمة", - "tr": "Hizmet Şartlarını Kabul Et" + "tr": "Hizmet Şartlarını Kabul Et", + "ca": "Accepta les condicions del servei" }, "TOS$ACCEPT_TERMS_DESCRIPTION": { "en": "Please review and accept our terms of service before continuing", @@ -10925,7 +11607,8 @@ "uk": "Будь ласка, ознайомтеся та прийміть наші умови надання послуг, перш ніж продовжити", "no": "Vennligst gjennomgå og godta våre vilkår for tjenesten før du fortsetter", "ar": "يرجى مراجعة وقبول شروط الخدمة الخاصة بنا قبل المتابعة", - "tr": "Devam etmeden önce lütfen hizmet şartlarımızı gözden geçirin ve kabul edin" + "tr": "Devam etmeden önce lütfen hizmet şartlarımızı gözden geçirin ve kabul edin", + "ca": "Reviseu i accepteu les condicions del servei abans de continuar" }, "TOS$CONTINUE": { "en": "Continue", @@ -10941,7 +11624,8 @@ "uk": "Продовжити", "no": "Fortsett", "ar": "متابعة", - "tr": "Devam Et" + "tr": "Devam Et", + "ca": "Continua" }, "TOS$ERROR_ACCEPTING": { "en": "Error accepting Terms of Service", @@ -10957,7 +11641,8 @@ "uk": "Помилка прийняття Умов обслуговування", "no": "Feil ved godkjenning av vilkår for tjenesten", "ar": "خطأ في قبول شروط الخدمة", - "tr": "Hizmet Şartlarını kabul ederken hata oluştu" + "tr": "Hizmet Şartlarını kabul ederken hata oluştu", + "ca": "Error en acceptar les condicions del servei" }, "TIPS$CUSTOMIZE_MICROAGENT": { "en": "You can customize OpenHands for your repo using an available microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.", @@ -10973,7 +11658,8 @@ "de": "Sie können OpenHands für Ihr Repository mit einem verfügbaren Mikroagenten anpassen. Bitten Sie OpenHands, eine Beschreibung des Repositorys, einschließlich der Ausführung des Codes, in .openhands/microagents/repo.md zu platzieren.", "fr": "Vous pouvez personnaliser OpenHands pour votre dépôt en utilisant un micro-agent disponible. Demandez à OpenHands de mettre une description du dépôt, y compris comment exécuter le code, dans .openhands/microagents/repo.md.", "tr": "Kullanılabilir bir mikro ajan kullanarak OpenHands'i deponuz için özelleştirebilirsiniz. OpenHands'ten deponun açıklamasını, kodun nasıl çalıştırılacağı dahil, .openhands/microagents/repo.md dosyasına koymasını isteyin.", - "uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md." + "uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md.", + "ca": "Podeu personalitzar OpenHands per al vostre repositori fent servir un microagent disponible. Demaneu a OpenHands que posi una descripció del repositori, incloent com executar el codi, a .openhands/microagents/repo.md." }, "CONVERSATION$NO_SKILLS": { "en": "No available skills found for this conversation.", @@ -10989,7 +11675,8 @@ "pt": "Nenhuma habilidade disponível encontrada para esta conversa.", "es": "No se encontraron habilidades disponibles para esta conversación.", "tr": "Bu sohbet için kullanılabilir yetenek bulunamadı.", - "uk": "У цій розмові не знайдено доступних навичок." + "uk": "У цій розмові не знайдено доступних навичок.", + "ca": "No s'han trobat habilitats disponibles per a aquesta conversa." }, "CONVERSATION$NO_HOOKS": { "en": "No hooks configured for this conversation.", @@ -11005,7 +11692,8 @@ "pt": "Nenhum hook configurado para esta conversa.", "es": "No hay hooks configurados para esta conversación.", "tr": "Bu sohbet için yapılandırılmış kanca yok.", - "uk": "Для цієї розмови не налаштовано хуків." + "uk": "Для цієї розмови не налаштовано хуків.", + "ca": "No hi ha hooks configurats per a aquesta conversa." }, "CONVERSATION$SHOW_HOOKS": { "en": "Show Available Hooks", @@ -11021,7 +11709,8 @@ "pt": "Mostrar hooks disponíveis", "es": "Mostrar hooks disponibles", "tr": "Kullanılabilir kancaları göster", - "uk": "Показати доступні хуки" + "uk": "Показати доступні хуки", + "ca": "Mostra els hooks disponibles" }, "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": { "en": "Failed to fetch available microagents", @@ -11037,7 +11726,8 @@ "pt": "Falha ao buscar microagentes disponíveis", "es": "Error al obtener microagentes disponibles", "tr": "Kullanılabilir mikro ajanlar getirilemedi", - "uk": "Не вдалося отримати доступних мікроагентів" + "uk": "Не вдалося отримати доступних мікроагентів", + "ca": "No s'han pogut obtenir els microagents disponibles" }, "MICROAGENTS_MODAL$TITLE": { "en": "Available Microagents", @@ -11053,7 +11743,8 @@ "pt": "Microagentes disponíveis", "es": "Microagentes disponibles", "tr": "Kullanılabilir mikro ajanlar", - "uk": "Доступні мікроагенти" + "uk": "Доступні мікроагенти", + "ca": "Microagents disponibles" }, "SKILLS_MODAL$WARNING": { "en": "If you update the skills, you will need to stop the conversation and then click on the refresh button to see the changes.", @@ -11069,7 +11760,8 @@ "pt": "Se você atualizar as habilidades, precisará interromper a conversa e clicar no botão de atualizar para ver as mudanças.", "es": "Si actualizas las habilidades, deberás detener la conversación y luego hacer clic en el botón de actualizar para ver los cambios.", "tr": "Yetenekleri güncellerseniz, değişiklikleri görmek için sohbeti durdurmalı ve ardından yenile düğmesine tıklamalısınız.", - "uk": "Якщо ви оновите навички, вам потрібно буде зупинити розмову, а потім натиснути кнопку оновлення, щоб побачити зміни." + "uk": "Якщо ви оновите навички, вам потрібно буде зупинити розмову, а потім натиснути кнопку оновлення, щоб побачити зміни.", + "ca": "Si actualitzeu les habilitats, haureu d'aturar la conversa i fer clic al botó d'actualització per veure els canvis." }, "COMMON$TRIGGERS": { "en": "Triggers", @@ -11085,7 +11777,8 @@ "pt": "Gatilhos", "es": "Disparadores", "tr": "Tetikleyiciler", - "uk": "Тригери" + "uk": "Тригери", + "ca": "Disparadors" }, "MICROAGENTS_MODAL$INPUTS": { "en": "Inputs", @@ -11101,7 +11794,8 @@ "pt": "Entradas", "es": "Entradas", "tr": "Girdiler", - "uk": "Вхідні дані" + "uk": "Вхідні дані", + "ca": "Entrades" }, "MICROAGENTS_MODAL$TOOLS": { "en": "Tools", @@ -11117,7 +11811,8 @@ "pt": "Ferramentas", "es": "Herramientas", "tr": "Araçlar", - "uk": "Інструменти" + "uk": "Інструменти", + "ca": "Eines" }, "COMMON$CONTENT": { "en": "Content", @@ -11133,7 +11828,8 @@ "pt": "Conteúdo", "es": "Contenido", "tr": "İçerik", - "uk": "Вміст" + "uk": "Вміст", + "ca": "Contingut" }, "SKILLS_MODAL$NO_CONTENT": { "en": "Skill has no content", @@ -11149,7 +11845,8 @@ "pt": "A habilidade não possui conteúdo", "es": "La habilidad no tiene contenido", "tr": "Beceride içerik yok", - "uk": "У навички немає вмісту" + "uk": "У навички немає вмісту", + "ca": "L'habilitat no té contingut" }, "COMMON$FETCH_ERROR": { "en": "Failed to fetch skills. Please try again later.", @@ -11165,7 +11862,8 @@ "pt": "Falha ao buscar as habilidades. Por favor, tente novamente mais tarde.", "es": "No se pudieron obtener las habilidades. Por favor, inténtalo de nuevo más tarde.", "tr": "Beceriler alınamadı. Lütfen daha sonra tekrar deneyin.", - "uk": "Не вдалося отримати навички. Будь ласка, спробуйте пізніше." + "uk": "Не вдалося отримати навички. Будь ласка, спробуйте пізніше.", + "ca": "No s'han pogut obtenir les habilitats. Torneu-ho a intentar més tard." }, "TIPS$SETUP_SCRIPT": { "en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.", @@ -11181,7 +11879,8 @@ "fr": "Vous pouvez ajouter .openhands/setup.sh à votre dépôt pour exécuter automatiquement un script de configuration chaque fois que vous démarrez une conversation OpenHands.", "tr": "OpenHands konuşması başlattığınız her seferinde otomatik olarak bir kurulum betiği çalıştırmak için deponuza .openhands/setup.sh ekleyebilirsiniz.", "de": "Sie können .openhands/setup.sh zu Ihrem Repository hinzufügen, um jedes Mal, wenn Sie ein OpenHands-Gespräch starten, automatisch ein Setup-Skript auszuführen.", - "uk": "Ви можете додати .openhands/setup.sh до свого репозиторію, щоб автоматично запускати скрипт налаштування щоразу, коли ви починаєте розмову OpenHands." + "uk": "Ви можете додати .openhands/setup.sh до свого репозиторію, щоб автоматично запускати скрипт налаштування щоразу, коли ви починаєте розмову OpenHands.", + "ca": "Podeu afegir .openhands/setup.sh al vostre repositori per executar automàticament un script de configuració cada vegada que inicieu una conversa d'OpenHands." }, "TIPS$VSCODE_INSTANCE": { "en": "Every OpenHands conversation comes with a VS Code instance, where you can interact with the development environment.", @@ -11197,7 +11896,8 @@ "fr": "Chaque conversation OpenHands est accompagnée d'une instance VS Code, où vous pouvez interagir avec l'environnement de développement.", "tr": "Her OpenHands konuşması, geliştirme ortamıyla etkileşimde bulunabileceğiniz bir VS Code örneği ile birlikte gelir.", "de": "Jedes OpenHands-Gespräch wird mit einer VS Code-Instanz geliefert, in der Sie mit der Entwicklungsumgebung interagieren können.", - "uk": "Кожна розмова OpenHands постачається з екземпляром VS Code, де ви можете взаємодіяти із середовищем розробки." + "uk": "Кожна розмова OpenHands постачається з екземпляром VS Code, де ви можете взаємодіяти із середовищем розробки.", + "ca": "Cada conversa d'OpenHands inclou una instància de VS Code, on podeu interactuar amb l'entorn de desenvolupament." }, "TIPS$SAVE_WORK": { "en": "Be sure to regularly save your work, either by pushing to GitHub or by downloading your files via VS Code.", @@ -11213,7 +11913,8 @@ "fr": "Assurez-vous de sauvegarder régulièrement votre travail, soit en le poussant vers GitHub, soit en téléchargeant vos fichiers via VS Code.", "tr": "GitHub'a göndererek veya VS Code aracılığıyla dosyalarınızı indirerek çalışmalarınızı düzenli olarak kaydettiğinizden emin olun.", "de": "Stellen Sie sicher, dass Sie Ihre Arbeit regelmäßig speichern, entweder durch Pushen zu GitHub oder durch Herunterladen Ihrer Dateien über VS Code.", - "uk": "Обов’язково регулярно зберігайте свою роботу, або завантажуючи її на GitHub, або завантажуючи файли через VS Code." + "uk": "Обов’язково регулярно зберігайте свою роботу, або завантажуючи її на GitHub, або завантажуючи файли через VS Code.", + "ca": "Assegureu-vos de desar el treball regularment, ja sigui publicant-lo a GitHub o descarregant els fitxers a través de VS Code." }, "TIPS$SPECIFY_FILES": { "en": "When possible, include the names of files or functions OpenHands should focus on. This can help OpenHands work faster, save money, and improve accuracy.", @@ -11229,7 +11930,8 @@ "fr": "Lorsque c'est possible, incluez les noms des fichiers ou des fonctions sur lesquels OpenHands devrait se concentrer. Cela peut aider OpenHands à travailler plus rapidement, à économiser de l'argent et à améliorer la précision.", "tr": "Mümkün olduğunda, OpenHands'in odaklanması gereken dosya veya fonksiyon isimlerini dahil edin. Bu, OpenHands'in daha hızlı çalışmasına, para tasarrufu sağlamasına ve doğruluğu artırmasına yardımcı olabilir.", "de": "Wenn möglich, geben Sie die Namen der Dateien oder Funktionen an, auf die sich OpenHands konzentrieren soll. Dies kann OpenHands helfen, schneller zu arbeiten, Geld zu sparen und die Genauigkeit zu verbessern.", - "uk": "Коли це можливо, вказуйте назви файлів або функцій, на яких має зосередитися OpenHands. Це може допомогти OpenHands працювати швидше, заощадити гроші та підвищити точність." + "uk": "Коли це можливо, вказуйте назви файлів або функцій, на яких має зосередитися OpenHands. Це може допомогти OpenHands працювати швидше, заощадити гроші та підвищити точність.", + "ca": "Quan sigui possible, incloeu els noms dels fitxers o funcions en els quals OpenHands s'ha de centrar. Això pot ajudar OpenHands a treballar més ràpid, estalviar diners i millorar la precisió." }, "TIPS$HEADLESS_MODE": { "en": "You can run OpenHands in headless mode to create automations, like responding to 500 errors by automatically creating a fix.", @@ -11245,7 +11947,8 @@ "fr": "Vous pouvez exécuter OpenHands en mode headless pour créer des automatisations, comme répondre aux erreurs 500 en créant automatiquement un correctif.", "tr": "OpenHands'i başsız modda çalıştırarak, 500 hatalarına otomatik olarak düzeltme oluşturarak yanıt vermek gibi otomasyonlar oluşturabilirsiniz.", "de": "Sie können OpenHands im Headless-Modus ausführen, um Automatisierungen zu erstellen, wie z.B. das Reagieren auf 500-Fehler durch automatisches Erstellen einer Lösung.", - "uk": "Ви можете запускати OpenHands в headless режимі для створення автоматизації, наприклад, реагування на 500 помилок шляхом автоматичного створення виправлення." + "uk": "Ви можете запускати OpenHands в headless режимі для створення автоматизації, наприклад, реагування на 500 помилок шляхом автоматичного створення виправлення.", + "ca": "Podeu executar OpenHands en mode sense cap per crear automatitzacions, com respondre a errors 500 creant automàticament una solució." }, "TIPS$CLI_MODE": { "en": "You can run OpenHands as a CLI, similar to Claude Code.", @@ -11261,7 +11964,8 @@ "fr": "Vous pouvez exécuter OpenHands en tant que CLI, similaire à Claude Code.", "tr": "OpenHands'i Claude Code'a benzer şekilde bir CLI olarak çalıştırabilirsiniz.", "de": "Sie können OpenHands als CLI ausführen, ähnlich wie Claude Code.", - "uk": "Ви можете запускати OpenHands як CLI, подібно до Claude Code." + "uk": "Ви можете запускати OpenHands як CLI, подібно до Claude Code.", + "ca": "Podeu executar OpenHands com a CLI, similar a Claude Code." }, "TIPS$GITHUB_HOOK": { "en": "OpenHands Cloud offers a GitHub hook, so you can say \"@openhands fix the merge conflicts\" or \"@openhands fix the feedback on this PR\" right inside the GitHub UI.", @@ -11277,7 +11981,8 @@ "fr": "OpenHands Cloud propose un hook GitHub, vous pouvez donc dire \"@openhands fix the merge conflicts\" ou \"@openhands fix the feedback on this PR\" directement dans l'interface GitHub.", "tr": "OpenHands Cloud, GitHub kancası sunar, böylece GitHub arayüzünde doğrudan \"@openhands birleştirme çakışmalarını düzelt\" veya \"@openhands bu PR'daki geri bildirimi düzelt\" diyebilirsiniz.", "de": "OpenHands Cloud bietet einen GitHub-Hook, sodass Sie \"@openhands fix the merge conflicts\" oder \"@openhands fix the feedback on this PR\" direkt in der GitHub-Benutzeroberfläche sagen können.", - "uk": "OpenHands Cloud пропонує хук GitHub, тож ви можете сказати «@openhands виправити конфлікти злиття» або «@openhands виправити відгук про цей PR» прямо в інтерфейсі GitHub." + "uk": "OpenHands Cloud пропонує хук GitHub, тож ви можете сказати «@openhands виправити конфлікти злиття» або «@openhands виправити відгук про цей PR» прямо в інтерфейсі GitHub.", + "ca": "OpenHands Cloud ofereix un hook de GitHub, de manera que podeu dir \"@openhands corregeix els conflictes de fusió\" o \"@openhands corregeix els comentaris d'aquesta PR\" directament dins de la interfície de GitHub." }, "TIPS$BLOG_SIGNUP": { "en": "Sign up for the OpenHands Blog to hear about new features and the latest releases.", @@ -11293,7 +11998,8 @@ "fr": "Inscrivez-vous au blog OpenHands pour connaître les nouvelles fonctionnalités et les dernières versions.", "tr": "Yeni özellikler ve en son sürümler hakkında bilgi almak için OpenHands Blog'a kaydolun.", "de": "Melden Sie sich für den OpenHands Blog an, um über neue Funktionen und die neuesten Versionen informiert zu werden.", - "uk": "Підпишіться на блог OpenHands, щоб дізнаватися про нові функції та останні релізи." + "uk": "Підпишіться на блог OpenHands, щоб дізнаватися про нові функції та останні релізи.", + "ca": "Subscriviu-vos al blog d'OpenHands per assabentar-vos de les noves funcions i les darreres versions." }, "TIPS$API_USAGE": { "en": "OpenHands has an API! Create OpenHands conversations with simple cURL command.", @@ -11309,7 +12015,8 @@ "fr": "OpenHands a une API ! Créez des conversations OpenHands avec une simple commande cURL.", "tr": "OpenHands'in bir API'si var! Basit bir cURL komutuyla OpenHands konuşmaları oluşturun.", "de": "OpenHands hat eine API! Erstellen Sie OpenHands-Gespräche mit einem einfachen cURL-Befehl.", - "uk": "OpenHands має API! Створюйте розмови OpenHands за допомогою простої команди cURL." + "uk": "OpenHands має API! Створюйте розмови OpenHands за допомогою простої команди cURL.", + "ca": "OpenHands té una API! Creeu converses d'OpenHands amb una senzilla comanda cURL." }, "TIPS$LEARN_MORE": { "en": "Learn more", @@ -11325,7 +12032,8 @@ "fr": "En savoir plus", "tr": "Daha fazla bilgi", "de": "Mehr erfahren", - "uk": "Дізнатися більше" + "uk": "Дізнатися більше", + "ca": "Aprèn-ne més" }, "TIPS$PROTIP": { "en": "Protip", @@ -11341,7 +12049,8 @@ "fr": "Astuce pro", "tr": "Uzman ipucu", "de": "Profi-Tipp", - "uk": "Порада професіонала" + "uk": "Порада професіонала", + "ca": "Consell" }, "FEEDBACK$SUBMITTING_LABEL": { "en": "Submitting...", @@ -11357,7 +12066,8 @@ "fr": "Envoi...", "tr": "Gönderiliyor...", "de": "Senden...", - "uk": "Відправляємо..." + "uk": "Відправляємо...", + "ca": "Enviant..." }, "FEEDBACK$SUBMITTING_MESSAGE": { "en": "Submitting feedback, please wait...", @@ -11373,7 +12083,8 @@ "fr": "Envoi de commentaires, veuillez patienter...", "tr": "Geri bildirim gönderiliyor, lütfen bekleyin...", "de": "Feedback senden, bitte warten...", - "uk": "Відправляємо відгук, будь ласка, почекайте..." + "uk": "Відправляємо відгук, будь ласка, почекайте...", + "ca": "Enviant els comentaris, espereu..." }, "SETTINGS$NAV_ADD_TEAM_MEMBERS": { "en": "Add Team Members", @@ -11389,7 +12100,8 @@ "fr": "Ajouter des membres de l'équipe", "tr": "Takım üyeleri ekle", "de": "Teammitglieder hinzufügen", - "uk": "Додати учасників команди" + "uk": "Додати учасників команди", + "ca": "Afegeix membres de l'equip" }, "SETTINGS$NAV_USER": { "en": "User", @@ -11405,7 +12117,8 @@ "fr": "Utilisateur", "tr": "Kullanıcı", "de": "Benutzer", - "uk": "Користувач" + "uk": "Користувач", + "ca": "Usuari" }, "SETTINGS$USER_TITLE": { "en": "User Information", @@ -11421,7 +12134,8 @@ "fr": "Informations utilisateur", "tr": "Kullanıcı Bilgileri", "de": "Benutzerinformationen", - "uk": "Інформація про користувача" + "uk": "Інформація про користувача", + "ca": "Informació de l'usuari" }, "SETTINGS$USER_EMAIL": { "en": "Email", @@ -11437,7 +12151,8 @@ "fr": "Email", "tr": "E-posta", "de": "E-Mail", - "uk": "Електронна пошта" + "uk": "Електронна пошта", + "ca": "Correu electrònic" }, "SETTINGS$USER_EMAIL_LOADING": { "en": "Loading...", @@ -11453,7 +12168,8 @@ "fr": "Chargement...", "tr": "Yükleniyor...", "de": "Wird geladen...", - "uk": "Завантаження..." + "uk": "Завантаження...", + "ca": "Carregant..." }, "SETTINGS$SAVE": { "en": "Save", @@ -11469,7 +12185,8 @@ "fr": "Enregistrer", "tr": "Kaydet", "de": "Speichern", - "uk": "Зберегти" + "uk": "Зберегти", + "ca": "Desa" }, "SETTINGS$EMAIL_SAVED_SUCCESSFULLY": { "en": "Email saved successfully", @@ -11485,7 +12202,8 @@ "fr": "Email enregistré avec succès", "tr": "E-posta başarıyla kaydedildi", "de": "E-Mail erfolgreich gespeichert", - "uk": "Електронну пошту успішно збережено" + "uk": "Електронну пошту успішно збережено", + "ca": "Correu electrònic desat correctament" }, "SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY": { "en": "Your email has been verified successfully!", @@ -11501,7 +12219,8 @@ "fr": "Votre email a été vérifié avec succès !", "tr": "E-postanız başarıyla doğrulandı!", "de": "Ihre E-Mail wurde erfolgreich verifiziert!", - "uk": "Вашу електронну пошту успішно підтверджено!" + "uk": "Вашу електронну пошту успішно підтверджено!", + "ca": "El vostre correu electrònic s'ha verificat correctament!" }, "SETTINGS$FAILED_TO_SAVE_EMAIL": { "en": "Failed to save email", @@ -11517,7 +12236,8 @@ "fr": "Échec de l'enregistrement de l'email", "tr": "E-posta kaydedilemedi", "de": "E-Mail konnte nicht gespeichert werden", - "uk": "Не вдалося зберегти електронну пошту" + "uk": "Не вдалося зберегти електронну пошту", + "ca": "No s'ha pogut desar el correu electrònic" }, "SETTINGS$SENDING": { "en": "Sending", @@ -11533,7 +12253,8 @@ "fr": "Envoi en cours", "tr": "Gönderiliyor", "de": "Wird gesendet", - "uk": "Надсилання" + "uk": "Надсилання", + "ca": "Enviant" }, "SETTINGS$VERIFICATION_EMAIL_SENT": { "en": "Verification email sent", @@ -11549,7 +12270,8 @@ "fr": "Email de vérification envoyé", "tr": "Doğrulama e-postası gönderildi", "de": "Bestätigungs-E-Mail gesendet", - "uk": "Лист підтвердження надіслано" + "uk": "Лист підтвердження надіслано", + "ca": "Correu electrònic de verificació enviat" }, "SETTINGS$EMAIL_VERIFICATION_REQUIRED": { "en": "You must verify your email address before using All Hands", @@ -11565,7 +12287,8 @@ "fr": "Vous devez vérifier votre adresse e-mail avant d'utiliser All Hands", "tr": "All Hands'i kullanmadan önce e-posta adresinizi doğrulamanız gerekiyor", "de": "Sie müssen Ihre E-Mail-Adresse bestätigen, bevor Sie All Hands verwenden können", - "uk": "Ви повинні підтвердити свою електронну адресу перед використанням All Hands" + "uk": "Ви повинні підтвердити свою електронну адресу перед використанням All Hands", + "ca": "Heu de verificar la vostra adreça de correu electrònic abans de fer servir All Hands" }, "SETTINGS$INVALID_EMAIL_FORMAT": { "en": "Please enter a valid email address", @@ -11581,7 +12304,8 @@ "fr": "Veuillez entrer une adresse e-mail valide", "tr": "Lütfen geçerli bir e-posta adresi girin", "de": "Bitte geben Sie eine gültige E-Mail-Adresse ein", - "uk": "Будь ласка, введіть дійсну електронну адресу" + "uk": "Будь ласка, введіть дійсну електронну адресу", + "ca": "Introduïu una adreça de correu electrònic vàlida" }, "SETTINGS$EMAIL_VERIFICATION_RESTRICTION_MESSAGE": { "en": "Your access is limited until your email is verified. You can only access this settings page.", @@ -11597,7 +12321,8 @@ "fr": "Votre accès est limité jusqu'à ce que votre e-mail soit vérifié. Vous ne pouvez accéder qu'à cette page de paramètres.", "tr": "E-postanız doğrulanana kadar erişiminiz sınırlıdır. Yalnızca bu ayarlar sayfasına erişebilirsiniz.", "de": "Ihr Zugriff ist eingeschränkt, bis Ihre E-Mail-Adresse bestätigt wurde. Sie können nur auf diese Einstellungsseite zugreifen.", - "uk": "Ваш доступ обмежений, доки ваша електронна пошта не буде підтверджена. Ви можете отримати доступ лише до цієї сторінки налаштувань." + "uk": "Ваш доступ обмежений, доки ваша електронна пошта не буде підтверджена. Ви можете отримати доступ лише до цієї сторінки налаштувань.", + "ca": "El vostre accés és limitat fins que el correu electrònic sigui verificat. Només podeu accedir a aquesta pàgina de configuració." }, "SETTINGS$RESEND_VERIFICATION": { "en": "Resend verification", @@ -11613,7 +12338,8 @@ "fr": "Renvoyer la vérification", "tr": "Doğrulamayı yeniden gönder", "de": "Bestätigung erneut senden", - "uk": "Надіслати підтвердження повторно" + "uk": "Надіслати підтвердження повторно", + "ca": "Reenvieu la verificació" }, "SETTINGS$FAILED_TO_RESEND_VERIFICATION": { "en": "Failed to resend verification email", @@ -11629,7 +12355,8 @@ "fr": "Échec du renvoi de l'email de vérification", "tr": "Doğrulama e-postası yeniden gönderilemedi", "de": "Bestätigungs-E-Mail konnte nicht erneut gesendet werden", - "uk": "Не вдалося повторно надіслати лист підтвердження" + "uk": "Не вдалося повторно надіслати лист підтвердження", + "ca": "No s'ha pogut reenviar el correu electrònic de verificació" }, "FEEDBACK$RATE_AGENT_PERFORMANCE": { "en": "Rate the agent's performance:", @@ -11645,7 +12372,8 @@ "fr": "Évaluez la performance de l'agent :", "tr": "Ajanın performansını değerlendirin:", "de": "Bewerten Sie die Leistung des Agenten:", - "uk": "Оцініть продуктивність агента:" + "uk": "Оцініть продуктивність агента:", + "ca": "Valoreu el rendiment de l'agent:" }, "FEEDBACK$SELECT_REASON": { "en": "Select a reason (optional):", @@ -11661,7 +12389,8 @@ "fr": "Sélectionnez une raison (facultatif) :", "tr": "Bir neden seçin (isteğe bağlı):", "de": "Wählen Sie einen Grund (optional):", - "uk": "Виберіть причину (необов'язково):" + "uk": "Виберіть причину (необов'язково):", + "ca": "Seleccioneu un motiu (opcional):" }, "FEEDBACK$SELECT_REASON_COUNTDOWN": { "en": "Auto-submitting in {{countdown}} seconds...", @@ -11677,7 +12406,8 @@ "fr": "Envoi automatique dans {{countdown}} secondes...", "tr": "{{countdown}} saniye içinde otomatik gönderilecek...", "de": "Automatische Übermittlung in {{countdown}} Sekunden...", - "uk": "Автоматична відправка через {{countdown}} секунд..." + "uk": "Автоматична відправка через {{countdown}} секунд...", + "ca": "S'enviarà automàticament en {{countdown}} segons..." }, "FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION": { "en": "The agent misunderstood my instruction", @@ -11693,7 +12423,8 @@ "fr": "L'agent a mal compris mes instructions", "tr": "Ajan talimatlarımı yanlış anladı", "de": "Der Agent hat meine Anweisungen missverstanden", - "uk": "Агент неправильно зрозумів мої інструкції" + "uk": "Агент неправильно зрозумів мої інструкції", + "ca": "L'agent no ha entès les meves instruccions" }, "FEEDBACK$REASON_FORGOT_CONTEXT": { "en": "The agent forgot about the earlier context", @@ -11709,7 +12440,8 @@ "fr": "L'agent a oublié le contexte précédent", "tr": "Ajan önceki bağlamı unuttu", "de": "Der Agent hat den früheren Kontext vergessen", - "uk": "Агент забув про попередній контекст" + "uk": "Агент забув про попередній контекст", + "ca": "L'agent ha oblidat el context anterior" }, "FEEDBACK$REASON_UNNECESSARY_CHANGES": { "en": "The agent made unnecessary changes", @@ -11725,7 +12457,8 @@ "fr": "L'agent a apporté des modifications inutiles", "tr": "Ajan gereksiz değişiklikler yaptı", "de": "Der Agent hat unnötige Änderungen vorgenommen", - "uk": "Агент зробив непотрібні зміни" + "uk": "Агент зробив непотрібні зміни", + "ca": "L'agent ha fet canvis innecessaris" }, "FEEDBACK$REASON_SHOULD_ASK_FIRST": { "en": "The agent should've asked me first before doing it!", @@ -11741,7 +12474,8 @@ "fr": "L'agent aurait dû me demander d'abord avant de le faire !", "tr": "Ajan bunu yapmadan önce bana sormalıydı!", "de": "Der Agent hätte mich vorher fragen sollen!", - "uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!" + "uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!", + "ca": "L'agent hauria d'haver-me preguntat primer abans de fer-ho!" }, "FEEDBACK$REASON_DIDNT_FINISH_JOB": { "en": "The agent didn't finish the job", @@ -11757,7 +12491,8 @@ "fr": "L'agent n'a pas terminé le travail", "tr": "Ajan işi bitirmedi", "de": "Der Agent hat die Aufgabe nicht beendet", - "uk": "Агент не завершив роботу" + "uk": "Агент не завершив роботу", + "ca": "L'agent no ha acabat la feina" }, "FEEDBACK$REASON_OTHER": { "en": "Other", @@ -11773,7 +12508,8 @@ "fr": "Autre", "tr": "Diğer", "de": "Andere", - "uk": "Інше" + "uk": "Інше", + "ca": "Altre" }, "FEEDBACK$THANK_YOU_FOR_FEEDBACK": { "en": "Thank you for your feedback! This will help us improve OpenHands going forward.", @@ -11789,7 +12525,8 @@ "fr": "Merci pour votre retour ! Cela nous aidera à améliorer OpenHands à l'avenir.", "tr": "Geri bildiriminiz için teşekkürler! Bu, OpenHands'i ileride geliştirmemize yardımcı olacak.", "de": "Vielen Dank für Ihr Feedback! Das hilft uns, OpenHands in Zukunft zu verbessern.", - "uk": "Дякуємо за ваш відгук! Це допоможе нам покращити OpenHands у майбутньому." + "uk": "Дякуємо за ваш відгук! Це допоможе нам покращити OpenHands у майбутньому.", + "ca": "Gràcies pels vostres comentaris! Això ens ajudarà a millorar OpenHands en el futur." }, "FEEDBACK$FAILED_TO_SUBMIT": { "en": "Failed to submit feedback", @@ -11805,7 +12542,8 @@ "fr": "Échec de l'envoi des commentaires", "tr": "Geri bildirim gönderilemedi", "de": "Feedback konnte nicht gesendet werden", - "uk": "Не вдалося надіслати відгук" + "uk": "Не вдалося надіслати відгук", + "ca": "No s'han pogut enviar els comentaris" }, "HOME$ADD_GITHUB_REPOS": { "en": "+ Add GitHub Repos", @@ -11821,7 +12559,8 @@ "fr": "+ Ajouter des dépôts GitHub", "tr": "+ GitHub depoları ekle", "de": "+ GitHub-Repositories hinzufügen", - "uk": "+ Додати репозиторії GitHub" + "uk": "+ Додати репозиторії GitHub", + "ca": "+ Afegeix repositoris de GitHub" }, "REPOSITORY$SELECT_BRANCH": { "en": "Select a branch", @@ -11837,7 +12576,8 @@ "fr": "Sélectionner une branche", "tr": "Bir dal seç", "de": "Einen Branch auswählen", - "uk": "Вибрати гілку" + "uk": "Вибрати гілку", + "ca": "Selecciona una branca" }, "REPOSITORY$SELECT_REPO": { "en": "Select a repo", @@ -11853,7 +12593,8 @@ "fr": "Sélectionner un dépôt", "tr": "Bir depo seç", "de": "Ein Repository auswählen", - "uk": "Вибрати репозиторій" + "uk": "Вибрати репозиторій", + "ca": "Selecciona un repositori" }, "TASKS$NO_GIT_PROVIDERS_TITLE": { "en": "Connect a Git provider", @@ -11869,7 +12610,8 @@ "pt": "Conectar um provedor Git", "es": "Conectar un proveedor Git", "tr": "Git sağlayıcısını bağla", - "uk": "Підключити постачальник Git" + "uk": "Підключити постачальник Git", + "ca": "Connecta un proveïdor de Git" }, "TASKS$NO_GIT_PROVIDERS_DESCRIPTION": { "en": "Connect a Git provider to see suggested tasks from your repositories.", @@ -11885,7 +12627,8 @@ "pt": "Conecte um provedor Git para ver tarefas sugeridas dos seus repositórios.", "es": "Conecta un proveedor Git para ver tareas sugeridas de tus repositorios.", "tr": "Depolarınızdan önerilen görevleri görmek için bir Git sağlayıcısı bağlayın.", - "uk": "Підключіть постачальник Git, щоб бачити запропоновані завдання з ваших репозиторіїв." + "uk": "Підключіть постачальник Git, щоб бачити запропоновані завдання з ваших репозиторіїв.", + "ca": "Connecteu un proveïdor de Git per veure les tasques suggerides dels vostres repositoris." }, "TASKS$NO_GIT_PROVIDERS_CTA": { "en": "Go to Integrations", @@ -11901,7 +12644,8 @@ "pt": "Ir para integrações", "es": "Ir a integraciones", "tr": "Entegrasyonlara git", - "uk": "Перейти до інтеграцій" + "uk": "Перейти до інтеграцій", + "ca": "Vés a Integracions" }, "TASKS$SUGGESTED_TASKS": { "en": "Suggested Tasks", @@ -11917,7 +12661,8 @@ "fr": "Tâches suggérées", "tr": "Önerilen görevler", "de": "Vorgeschlagene Aufgaben", - "uk": "Запропоновані завдання" + "uk": "Запропоновані завдання", + "ca": "Tasques suggerides" }, "TASKS$NO_TASKS_AVAILABLE": { "en": "No tasks available", @@ -11933,7 +12678,8 @@ "fr": "Aucune tâche disponible", "tr": "Mevcut görev yok", "de": "Keine Aufgaben verfügbar", - "uk": "Немає доступних завдань" + "uk": "Немає доступних завдань", + "ca": "No hi ha tasques disponibles" }, "TASKS$TASK_SUGGESTIONS_INFO": { "en": "Task suggestions information", @@ -11949,7 +12695,8 @@ "fr": "Informations sur les suggestions de tâches", "tr": "Görev önerisi bilgileri", "de": "Aufgabenvorschlag-Informationen", - "uk": "Інформація про пропозиції завдань" + "uk": "Інформація про пропозиції завдань", + "ca": "Informació sobre els suggeriments de tasques" }, "TASKS$TASK_SUGGESTIONS_TOOLTIP": { "en": "These are AI-curated task suggestions to help you get started with common development activities and best practices for your repository.", @@ -11965,7 +12712,8 @@ "fr": "Ce sont des suggestions de tâches curées par l'IA pour vous aider à commencer avec les activités de développement courantes et les meilleures pratiques pour votre dépôt.", "tr": "Bunlar, deponuz için yaygın geliştirme faaliyetleri ve en iyi uygulamalarla başlamanıza yardımcı olmak için AI tarafından düzenlenmiş görev önerileridir.", "de": "Dies sind KI-kuratierte Aufgabenvorschläge, die Ihnen helfen, mit gängigen Entwicklungsaktivitäten und bewährten Praktiken für Ihr Repository zu beginnen.", - "uk": "Це AI-курировані пропозиції завдань, які допоможуть вам розпочати з поширеними діяльностями розробки та найкращими практиками для вашого репозиторію." + "uk": "Це AI-курировані пропозиції завдань, які допоможуть вам розпочати з поширеними діяльностями розробки та найкращими практиками для вашого репозиторію.", + "ca": "Aquests són suggeriments de tasques seleccionats per IA per ajudar-vos a començar amb activitats de desenvolupament habituals i les millors pràctiques per al vostre repositori." }, "PAYMENT$SPECIFY_AMOUNT_USD": { "en": "Specify an amount in USD to add - min $10", @@ -11981,7 +12729,8 @@ "fr": "Spécifiez un montant en USD à ajouter - min 10 $", "tr": "Eklenecek USD tutarını belirtin - min $10", "de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10", - "uk": "Вкажіть суму в доларах США для додавання - мін $10" + "uk": "Вкажіть суму в доларах США для додавання - мін $10", + "ca": "Especifiqueu un import en USD per afegir - mínim 10 $" }, "PAYMENT$ERROR_INVALID_NUMBER": { "en": "Please enter a valid number", @@ -11997,7 +12746,8 @@ "fr": "Veuillez entrer un nombre valide", "tr": "Lütfen geçerli bir sayı girin", "de": "Bitte geben Sie eine gültige Zahl ein", - "uk": "Будь ласка, введіть дійсне число" + "uk": "Будь ласка, введіть дійсне число", + "ca": "Introduïu un número vàlid" }, "PAYMENT$ERROR_NEGATIVE_AMOUNT": { "en": "Amount cannot be negative", @@ -12013,7 +12763,8 @@ "fr": "Le montant ne peut pas être négatif", "tr": "Tutar negatif olamaz", "de": "Der Betrag darf nicht negativ sein", - "uk": "Сума не може бути від'ємною" + "uk": "Сума не може бути від'ємною", + "ca": "L'import no pot ser negatiu" }, "PAYMENT$ERROR_MINIMUM_AMOUNT": { "en": "Minimum amount is $10", @@ -12029,7 +12780,8 @@ "fr": "Le montant minimum est de 10 $", "tr": "Minimum tutar $10'dur", "de": "Der Mindestbetrag beträgt 10 $", - "uk": "Мінімальна сума становить $10" + "uk": "Мінімальна сума становить $10", + "ca": "L'import mínim és de $10" }, "PAYMENT$ERROR_MAXIMUM_AMOUNT": { "en": "Maximum amount is $25,000", @@ -12045,7 +12797,8 @@ "fr": "Le montant maximum est de 25 000 $", "tr": "Maksimum tutar $25,000'dur", "de": "Der Höchstbetrag beträgt 25.000 $", - "uk": "Максимальна сума становить $25,000" + "uk": "Максимальна сума становить $25,000", + "ca": "L'import màxim és de $25.000" }, "PAYMENT$ERROR_MUST_BE_WHOLE_NUMBER": { "en": "Amount must be a whole number", @@ -12061,7 +12814,8 @@ "fr": "Le montant doit être un nombre entier", "tr": "Tutar tam sayı olmalıdır", "de": "Der Betrag muss eine ganze Zahl sein", - "uk": "Сума повинна бути цілим числом" + "uk": "Сума повинна бути цілим числом", + "ca": "L'import ha de ser un número enter" }, "GIT$BITBUCKET_TOKEN_HELP_LINK": { "en": "Bitbucket token help link", @@ -12077,7 +12831,8 @@ "fr": "Lien d'aide pour le jeton Bitbucket", "tr": "Bitbucket token yardım bağlantısı", "de": "Bitbucket-Token-Hilfe-Link", - "uk": "Посилання на довідку токена Bitbucket" + "uk": "Посилання на довідку токена Bitbucket", + "ca": "Enllaç d'ajuda per al token de Bitbucket" }, "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK": { "en": "Bitbucket token see more link", @@ -12093,7 +12848,8 @@ "fr": "Lien pour en voir plus sur le jeton Bitbucket", "tr": "Bitbucket token daha fazla görme bağlantısı", "de": "Bitbucket-Token mehr sehen Link", - "uk": "Посилання для перегляду більше про токен Bitbucket" + "uk": "Посилання для перегляду більше про токен Bitbucket", + "ca": "Enlacça per veure més sobre el token de Bitbucket" }, "GIT$BITBUCKET_DC_TOKEN_HELP_LINK": { "en": "Bitbucket Data Center HTTP access token docs", @@ -12109,7 +12865,8 @@ "pt": "Documentação do token de acesso HTTP do Bitbucket Data Center", "es": "Documentación del token de acceso HTTP de Bitbucket Data Center", "tr": "Bitbucket Data Center HTTP erişim jetonu belgeleri", - "uk": "Документація HTTP-токена доступу Bitbucket Data Center" + "uk": "Документація HTTP-токена доступу Bitbucket Data Center", + "ca": "Documentació del token d'accés HTTP de Bitbucket Data Center" }, "GIT$GITHUB_TOKEN_HELP_LINK": { "en": "GitHub token help link", @@ -12125,7 +12882,8 @@ "fr": "Lien d'aide pour le jeton GitHub", "tr": "GitHub token yardım bağlantısı", "de": "GitHub-Token-Hilfe-Link", - "uk": "Посилання на довідку токена GitHub" + "uk": "Посилання на довідку токена GitHub", + "ca": "Enllaç d'ajuda per al token de GitHub" }, "GIT$GITHUB_TOKEN_SEE_MORE_LINK": { "en": "GitHub token see more link", @@ -12141,7 +12899,8 @@ "fr": "Lien pour en voir plus sur le jeton GitHub", "tr": "GitHub token daha fazla görme bağlantısı", "de": "GitHub-Token mehr sehen Link", - "uk": "Посилання для перегляду більше про токен GitHub" + "uk": "Посилання для перегляду більше про токен GitHub", + "ca": "Enlacça per veure més sobre el token de GitHub" }, "GIT$GITLAB_TOKEN_HELP_LINK": { "en": "Gitlab token help link", @@ -12157,7 +12916,8 @@ "fr": "Lien d'aide pour le jeton GitLab", "tr": "GitLab token yardım bağlantısı", "de": "GitLab-Token-Hilfe-Link", - "uk": "Посилання на довідку токена GitLab" + "uk": "Посилання на довідку токена GitLab", + "ca": "Enllaç d'ajuda per al token de GitLab" }, "GIT$GITLAB_TOKEN_SEE_MORE_LINK": { "en": "GitLab token see more link", @@ -12173,7 +12933,8 @@ "fr": "Lien pour en voir plus sur le jeton GitLab", "tr": "GitLab token daha fazla görme bağlantısı", "de": "GitLab-Token mehr sehen Link", - "uk": "Посилання для перегляду більше про токен GitLab" + "uk": "Посилання для перегляду більше про токен GitLab", + "ca": "Enlacça per veure més sobre el token de GitLab" }, "SECRETS$SECRET_ALREADY_EXISTS": { "en": "Secret already exists", @@ -12189,7 +12950,8 @@ "fr": "Le secret existe déjà", "tr": "Gizli anahtar zaten mevcut", "de": "Geheimnis existiert bereits", - "uk": "Секрет вже існує" + "uk": "Секрет вже існує", + "ca": "El secret ja existeix" }, "SECRETS$API_KEY_EXAMPLE": { "en": "e.g. OpenAI_API_Key", @@ -12205,7 +12967,8 @@ "fr": "ex. OpenAI_API_Key", "tr": "örn. OpenAI_API_Key", "de": "z.B. OpenAI_API_Key", - "uk": "наприклад OpenAI_API_Key" + "uk": "наприклад OpenAI_API_Key", + "ca": "p. ex. OpenAI_API_Key" }, "MODEL$CUSTOM_MODEL": { "en": "Custom Model", @@ -12221,7 +12984,8 @@ "fr": "Modèle personnalisé", "tr": "Özel model", "de": "Benutzerdefiniertes Modell", - "uk": "Користувацька модель" + "uk": "Користувацька модель", + "ca": "Model personalitzat" }, "SECURITY$SELECT_RISK_SEVERITY": { "en": "Select risk severity", @@ -12237,7 +13001,8 @@ "fr": "Sélectionner la gravité du risque", "tr": "Risk ciddiyetini seç", "de": "Risikoschweregrad auswählen", - "uk": "Вибрати ступінь ризику" + "uk": "Вибрати ступінь ризику", + "ca": "Selecciona la gravetat del risc" }, "SECURITY$DONT_ASK_CONFIRMATION": { "en": "Don't ask for confirmation", @@ -12253,7 +13018,8 @@ "fr": "Ne pas demander de confirmation", "tr": "Onay isteme", "de": "Nicht nach Bestätigung fragen", - "uk": "Не запитувати підтвердження" + "uk": "Не запитувати підтвердження", + "ca": "No demanis confirmació" }, "SETTINGS$MAXIMUM_BUDGET_USD": { "en": "Maximum budget per conversation in USD", @@ -12269,7 +13035,8 @@ "fr": "Budget maximum par conversation en USD", "tr": "Konuşma başına maksimum bütçe (USD)", "de": "Maximales Budget pro Gespräch in USD", - "uk": "Максимальний бюджет на розмову в доларах США" + "uk": "Максимальний бюджет на розмову в доларах США", + "ca": "Pressupost màxim per conversa en USD" }, "GIT$DISCONNECT_TOKENS": { "en": "Disconnect Tokens", @@ -12285,7 +13052,8 @@ "fr": "Déconnecter les jetons", "tr": "Token bağlantısını kes", "de": "Token trennen", - "uk": "Відключити токени" + "uk": "Відключити токени", + "ca": "Desconnecta els tokens" }, "API$TAVILY_KEY_EXAMPLE": { "en": "tvly-dev-...", @@ -12301,7 +13069,8 @@ "fr": "tvly-dev-...", "tr": "tvly-dev-...", "de": "tvly-dev-...", - "uk": "tvly-dev-..." + "uk": "tvly-dev-...", + "ca": "tvly-dev-..." }, "API$TVLY_KEY_EXAMPLE": { "en": "tvly-...", @@ -12317,7 +13086,8 @@ "fr": "tvly-...", "tr": "tvly-...", "de": "tvly-...", - "uk": "tvly-..." + "uk": "tvly-...", + "ca": "tvly-..." }, "SECRETS$CONNECT_GIT_PROVIDER": { "en": "Connect a Git provider to manage secrets", @@ -12333,7 +13103,8 @@ "fr": "Connecter un fournisseur Git pour gérer les secrets", "tr": "Gizli anahtarları yönetmek için bir Git sağlayıcısına bağlan", "de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten", - "uk": "Підключити провайдера Git для управління секретами" + "uk": "Підключити провайдера Git для управління секретами", + "ca": "Connecteu un proveïdor de Git per gestionar els secrets" }, "SETTINGS$OPENHANDS_API_KEYS": { "en": "OpenHands API Keys", @@ -12349,7 +13120,8 @@ "fr": "Clés API OpenHands", "tr": "OpenHands API Anahtarları", "de": "OpenHands API-Schlüssel", - "uk": "API-ключі OpenHands" + "uk": "API-ключі OpenHands", + "ca": "Claus d'API d'OpenHands" }, "CONVERSATION$BUDGET_USAGE_FORMAT": { "en": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})", @@ -12365,7 +13137,8 @@ "fr": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})", "tr": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})", "de": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})", - "uk": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})" + "uk": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})", + "ca": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})" }, "CONVERSATION$CACHE_HIT": { "en": "Cache Hit", @@ -12381,7 +13154,8 @@ "fr": "Cache Hit", "tr": "Önbellek İsabeti", "de": "Cache-Treffer", - "uk": "Кеш-хіт" + "uk": "Кеш-хіт", + "ca": "Encert de la memòria cau" }, "CONVERSATION$CACHE_WRITE": { "en": "Cache Write", @@ -12397,7 +13171,8 @@ "fr": "Écriture Cache", "tr": "Önbellek Yazma", "de": "Cache-Schreiben", - "uk": "Запис у кеш" + "uk": "Запис у кеш", + "ca": "Escriptura a la memòria cau" }, "BUTTON$CONFIRM": { "en": "Confirm", @@ -12413,7 +13188,8 @@ "fr": "Confirmer", "tr": "Onayla", "de": "Bestätigen", - "uk": "Підтвердити" + "uk": "Підтвердити", + "ca": "Confirma" }, "FORM$VALUE": { "en": "Value", @@ -12429,7 +13205,8 @@ "fr": "Valeur", "tr": "Değer", "de": "Wert", - "uk": "Значення" + "uk": "Значення", + "ca": "Valor" }, "FORM$DESCRIPTION": { "en": "Description", @@ -12445,7 +13222,8 @@ "fr": "Description", "tr": "Açıklama", "de": "Beschreibung", - "uk": "Опис" + "uk": "Опис", + "ca": "Descripció" }, "COMMON$OPTIONAL": { "en": "Optional", @@ -12461,7 +13239,8 @@ "fr": "Optionnel", "tr": "İsteğe Bağlı", "de": "Optional", - "uk": "Необов'язково" + "uk": "Необов'язково", + "ca": "Opcional" }, "BROWSER$SERVER_MESSAGE": { "en": "No web app running. Ask OpenHands to start your project's dev server (for example: npm run dev) to see your web application here.", @@ -12477,7 +13256,8 @@ "fr": "Aucune application web n'est en cours d'exécution. Demandez à OpenHands de démarrer le serveur de développement de votre projet (par exemple : npm run dev) pour voir votre application web ici.", "tr": "Çalışan web uygulaması yok. Web uygulamanızı burada görmek için OpenHands'ten projenizin geliştirme sunucusunu başlatmasını isteyin (örneğin: npm run dev).", "de": "Keine Web-App läuft. Bitten Sie OpenHands, den Entwicklungsserver Ihres Projekts zu starten (zum Beispiel: npm run dev), um Ihre Web-Anwendung hier anzuzeigen.", - "uk": "Веб-додаток не працює. Попросіть OpenHands запустити сервер розробки вашого проєкту (наприклад: npm run dev), щоб побачити ваш веб-додаток тут." + "uk": "Веб-додаток не працює. Попросіть OpenHands запустити сервер розробки вашого проєкту (наприклад: npm run dev), щоб побачити ваш веб-додаток тут.", + "ca": "No hi ha cap aplicació web en execució. Demaneu a OpenHands que iniciï el servidor de desenvolupament del vostre projecte (per exemple: npm run dev) per veure la vostra aplicació web aquí." }, "API$NO_KEY_AVAILABLE": { "en": "No API key available", @@ -12493,7 +13273,8 @@ "fr": "Aucune clé API disponible", "tr": "Kullanılabilir API anahtarı yok", "de": "Kein API-Schlüssel verfügbar", - "uk": "Немає доступного API-ключа" + "uk": "Немає доступного API-ключа", + "ca": "No hi ha cap clau d'API disponible" }, "MICROAGENT_MANAGEMENT$TITLE": { "en": "Microagent Management", @@ -12509,7 +13290,8 @@ "fr": "Gestion des microagents", "tr": "Mikroajan Yönetimi", "de": "Microagent-Verwaltung", - "uk": "Управління мікроагентами" + "uk": "Управління мікроагентами", + "ca": "Gestió de Microagents" }, "MICROAGENT_MANAGEMENT$DESCRIPTION": { "en": "Manage Microagents", @@ -12525,7 +13307,8 @@ "fr": "Gérer les microagents", "tr": "Mikro ajanları yönet", "de": "Microagents verwalten", - "uk": "Керування мікроагентами" + "uk": "Керування мікроагентами", + "ca": "Gestiona els Microagents" }, "MICROAGENT_MANAGEMENT$USE_MICROAGENTS": { "en": "Use microagents to customize the behavior of OpenHands, teach it about your repositories, and help it work faster.", @@ -12541,7 +13324,8 @@ "fr": "Utilisez des microagents pour personnaliser le comportement d'OpenHands, lui apprendre vos dépôts et l'aider à travailler plus rapidement.", "tr": "OpenHands'in davranışını özelleştirmek, depolarınızı ona öğretmek ve daha hızlı çalışmasına yardımcı olmak için mikro ajanları kullanın.", "de": "Verwenden Sie Microagents, um das Verhalten von OpenHands anzupassen, ihm Ihre Repositories beizubringen und ihm zu helfen, schneller zu arbeiten.", - "uk": "Використовуйте мікроагенти, щоб налаштувати поведінку OpenHands, навчити його про ваші репозиторії та допомогти йому працювати швидше." + "uk": "Використовуйте мікроагенти, щоб налаштувати поведінку OpenHands, навчити його про ваші репозиторії та допомогти йому працювати швидше.", + "ca": "Feu servir els microagents per personalitzar el comportament d'OpenHands, ensenyar-li sobre els vostres repositoris i ajudar-lo a treballar més ràpid." }, "AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR": { "en": "By signing up, you agree to our", @@ -12557,7 +13341,8 @@ "fr": "En vous inscrivant, vous acceptez nos", "tr": "Kaydolarak, hizmet şartlarımızı kabul etmiş olursunuz", "de": "Mit der Anmeldung stimmen Sie unseren", - "uk": "Реєструючись, ви погоджуєтеся з нашими" + "uk": "Реєструючись, ви погоджуєтеся з нашими", + "ca": "En registrar-vos, accepteu les nostres" }, "AUTH$NO_PROVIDERS_CONFIGURED": { "en": "At least one identity provider must be configured (e.g., GitHub)", @@ -12573,7 +13358,8 @@ "fr": "Au moins un fournisseur d'identité doit être configuré (ex: GitHub)", "tr": "En az bir kimlik sağlayıcı yapılandırılmalıdır (örn. GitHub)", "de": "Mindestens ein Identitätsanbieter muss konfiguriert werden (z.B. GitHub)", - "uk": "Принаймні один постачальник ідентифікації має бути налаштований (наприклад, GitHub)" + "uk": "Принаймні один постачальник ідентифікації має бути налаштований (наприклад, GitHub)", + "ca": "S'ha de configurar almenys un proveïdor d'identitat (p. ex., GitHub)" }, "AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY": { "en": "Please check your email to verify your account.", @@ -12589,7 +13375,8 @@ "fr": "Veuillez vérifier votre e-mail pour vérifier votre compte.", "tr": "Hesabınızı doğrulamak için lütfen e-postanızı kontrol edin.", "de": "Bitte überprüfen Sie Ihre E-Mail, um Ihr Konto zu verifizieren.", - "uk": "Будь ласка, перевірте свою електронну пошту, щоб підтвердити свій обліковий запис." + "uk": "Будь ласка, перевірте свою електронну пошту, щоб підтвердити свій обліковий запис.", + "ca": "Comproveu el vostre correu electrònic per verificar el vostre compte." }, "AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL": { "en": "Please check your inbox for the verification email we sent earlier.", @@ -12605,7 +13392,8 @@ "fr": "Veuillez vérifier votre boîte de réception pour l'e-mail de vérification que nous vous avons envoyé précédemment.", "tr": "Lütfen daha önce gönderdiğimiz doğrulama e-postası için gelen kutunuzu kontrol edin.", "de": "Bitte überprüfen Sie Ihren Posteingang auf die Bestätigungs-E-Mail, die wir Ihnen zuvor gesendet haben.", - "uk": "Будь ласка, перевірте вашу поштову скриньку на наявність листа підтвердження, який ми надіслали раніше." + "uk": "Будь ласка, перевірте вашу поштову скриньку на наявність листа підтвердження, який ми надіслали раніше.", + "ca": "Comproveu la vostra safata d'entrada per al correu electrònic de verificació que hem enviat anteriorment." }, "AUTH$EMAIL_VERIFIED_PLEASE_LOGIN": { "en": "Your email has been verified. Please login below.", @@ -12621,7 +13409,8 @@ "fr": "Votre e-mail a été vérifié. Veuillez vous connecter ci-dessous.", "tr": "E-postanız doğrulandı. Lütfen aşağıdan giriş yapın.", "de": "Ihre E-Mail wurde verifiziert. Bitte melden Sie sich unten an.", - "uk": "Вашу електронну пошту підтверджено. Будь ласка, увійдіть нижче." + "uk": "Вашу електронну пошту підтверджено. Будь ласка, увійдіть нижче.", + "ca": "El vostre correu electrònic s'ha verificat. Inicieu sessió a continuació." }, "AUTH$DUPLICATE_EMAIL_ERROR": { "en": "An account with that email address already exists.", @@ -12637,7 +13426,8 @@ "fr": "Un compte avec cette adresse e-mail existe déjà.", "tr": "Bu e-posta adresine sahip bir hesap zaten mevcut.", "de": "Für diese E-Mail-Adresse existiert bereits ein Konto.", - "uk": "Обліковий запис з цією електронною адресою вже існує." + "uk": "Обліковий запис з цією електронною адресою вже існує.", + "ca": "Ja existeix un compte amb aquesta adreça de correu electrònic." }, "AUTH$RECAPTCHA_BLOCKED": { "en": "Access blocked due to suspicious activity. If you believe this is an error, please contact contact@openhands.dev for assistance.", @@ -12653,7 +13443,8 @@ "fr": "Accès bloqué en raison d'une activité suspecte. Si vous pensez qu'il s'agit d'une erreur, veuillez contacter contact@openhands.dev pour obtenir de l'aide.", "tr": "Şüpheli aktivite nedeniyle erişim engellendi. Bunun bir hata olduğunu düşünüyorsanız, yardım için lütfen contact@openhands.dev ile iletişime geçin.", "de": "Zugriff aufgrund verdächtiger Aktivitäten blockiert. Wenn Sie glauben, dass dies ein Fehler ist, wenden Sie sich bitte an contact@openhands.dev.", - "uk": "Доступ заблоковано через підозрілу активність. Якщо ви вважаєте, що це помилка, зверніться до contact@openhands.dev за допомогою." + "uk": "Доступ заблоковано через підозрілу активність. Якщо ви вважаєте, що це помилка, зверніться до contact@openhands.dev за допомогою.", + "ca": "Accés bloquejat a causa d'activitat sospitosa. Si creieu que és un error, contacteu amb contact@openhands.dev per obtenir ajuda." }, "AUTH$LETS_GET_STARTED": { "en": "Let's get started", @@ -12669,7 +13460,8 @@ "fr": "Commençons", "tr": "Başlayalım", "de": "Lass uns anfangen", - "uk": "Почнімо" + "uk": "Почнімо", + "ca": "Comencem" }, "AUTH$INVITATION_PENDING": { "en": "Sign in to accept your organization invitation", @@ -12685,7 +13477,8 @@ "fr": "Connectez-vous pour accepter l'invitation de votre organisation", "tr": "Organizasyon davetinizi kabul etmek için giriş yapın", "de": "Melden Sie sich an, um Ihre Organisationseinladung anzunehmen", - "uk": "Увійдіть, щоб прийняти запрошення до організації" + "uk": "Увійдіть, щоб прийняти запрошення до організації", + "ca": "Inicieu sessió per acceptar la invitació de la vostra organització" }, "AUTH$BITBUCKET_SIGNUP_DISABLED": { "en": "OpenHands Cloud has temporarily disabled Bitbucket registrations and is only accepting logins from existing users at this time. We recommend registering with GitHub or GitLab instead. We are sorry for the inconvenience.", @@ -12701,7 +13494,8 @@ "fr": "OpenHands Cloud a temporairement désactivé les inscriptions Bitbucket et n'accepte actuellement que les connexions des utilisateurs existants. Nous vous recommandons de vous inscrire avec GitHub ou GitLab à la place. Nous nous excusons pour la gêne occasionnée.", "tr": "OpenHands Cloud, Bitbucket kayıtlarını geçici olarak devre dışı bıraktı ve şu anda yalnızca mevcut kullanıcıların girişlerini kabul ediyor. Bunun yerine GitHub veya GitLab ile kayıt olmanızı öneririz. Verdiğimiz rahatsızlık için özür dileriz.", "de": "OpenHands Cloud hat Bitbucket-Registrierungen vorübergehend deaktiviert und akzeptiert derzeit nur Anmeldungen von bestehenden Benutzern. Wir empfehlen, sich stattdessen mit GitHub oder GitLab zu registrieren. Wir entschuldigen uns für die Unannehmlichkeiten.", - "uk": "OpenHands Cloud тимчасово вимкнув реєстрацію через Bitbucket і наразі приймає лише вхід існуючих користувачів. Рекомендуємо зареєструватися через GitHub або GitLab. Вибачте за незручності." + "uk": "OpenHands Cloud тимчасово вимкнув реєстрацію через Bitbucket і наразі приймає лише вхід існуючих користувачів. Рекомендуємо зареєструватися через GitHub або GitLab. Вибачте за незручності.", + "ca": "OpenHands Cloud ha desactivat temporalment els registres de Bitbucket i només accepta inicis de sessió d'usuaris existents en aquest moment. Recomanem registrar-vos amb GitHub o GitLab. Disculpeu les molèsties." }, "COMMON$TERMS_OF_SERVICE": { "en": "Terms of Service", @@ -12717,7 +13511,8 @@ "fr": "Conditions d'utilisation", "tr": "Hizmet Şartları", "de": "Nutzungsbedingungen", - "uk": "Умови надання послуг" + "uk": "Умови надання послуг", + "ca": "Condicions del servei" }, "COMMON$AND": { "en": "and", @@ -12733,7 +13528,8 @@ "fr": "et", "tr": "ve", "de": "und", - "uk": "та" + "uk": "та", + "ca": "i" }, "COMMON$PRIVACY_POLICY": { "en": "Privacy Policy", @@ -12749,7 +13545,8 @@ "fr": "Politique de confidentialité", "tr": "Gizlilik Politikası", "de": "Datenschutzrichtlinie", - "uk": "Політика конфіденційності" + "uk": "Політика конфіденційності", + "ca": "Política de privadesa" }, "COMMON$PERSONAL": { "en": "Personal", @@ -12765,7 +13562,8 @@ "fr": "Personnel", "tr": "Kişisel", "de": "Persönlich", - "uk": "Особистий" + "uk": "Особистий", + "ca": "Personal" }, "COMMON$REPOSITORIES": { "en": "Repositories", @@ -12781,7 +13579,8 @@ "fr": "Dépôts", "tr": "Depolar", "de": "Repositories", - "uk": "Репозиторії" + "uk": "Репозиторії", + "ca": "Repositoris" }, "COMMON$ORGANIZATIONS": { "en": "Organizations", @@ -12797,7 +13596,8 @@ "fr": "Organisations", "tr": "Organizasyonlar", "de": "Organisationen", - "uk": "Організації" + "uk": "Організації", + "ca": "Organitzacions" }, "COMMON$ORGANIZATION": { "en": "Organization", @@ -12813,7 +13613,8 @@ "fr": "Organisation", "tr": "Organizasyon", "de": "Organisation", - "uk": "Організація" + "uk": "Організація", + "ca": "Organització" }, "COMMON$ADD_MICROAGENT": { "en": "Add Microagent", @@ -12829,7 +13630,8 @@ "fr": "Ajouter un microagent", "tr": "Mikro ajan ekle", "de": "Microagent hinzufügen", - "uk": "Додати мікроагента" + "uk": "Додати мікроагента", + "ca": "Afegeix un Microagent" }, "COMMON$CREATED_ON": { "en": "Created on", @@ -12845,7 +13647,8 @@ "fr": "Créé", "tr": "Oluşturuldu", "de": "Erstellt", - "uk": "Створено" + "uk": "Створено", + "ca": "Creat el" }, "MICROAGENT_MANAGEMENT$LEARN_THIS_REPO": { "en": "Learn this repo", @@ -12861,7 +13664,8 @@ "fr": "Apprendre ce dépôt", "tr": "Bu depoyu öğren", "de": "Dieses Repository lernen", - "uk": "Вивчити цей репозиторій" + "uk": "Вивчити цей репозиторій", + "ca": "Aprèn d'aquest repositori" }, "MICROAGENT_MANAGEMENT$READY_TO_ADD_MICROAGENT": { "en": "Ready to add a microagent?", @@ -12877,7 +13681,8 @@ "fr": "Prêt à ajouter un microagent ?", "tr": "Bir mikro ajan eklemeye hazır mısınız?", "de": "Bereit, einen Microagent hinzuzufügen?", - "uk": "Готові додати мікроагента?" + "uk": "Готові додати мікроагента?", + "ca": "Preparat per afegir un microagent?" }, "MICROAGENT_MANAGEMENT$OPENHANDS_CAN_LEARN_ABOUT_REPOSITORIES": { "en": "OpenHands can automatically learn about your repositories and store its findings as markdown in a microagent. The microagent will help OpenHands run faster and more accurately in any future conversations.", @@ -12893,7 +13698,8 @@ "fr": "OpenHands peut automatiquement apprendre à connaître vos dépôts et stocker ses découvertes en markdown dans un microagent. Le microagent aidera OpenHands à fonctionner plus rapidement et plus précisément lors de futures conversations.", "tr": "OpenHands, depolarınızı otomatik olarak öğrenebilir ve bulgularını bir mikro ajan içinde markdown olarak saklayabilir. Mikro ajan, OpenHands'in gelecekteki konuşmalarda daha hızlı ve daha doğru çalışmasına yardımcı olur.", "de": "OpenHands kann automatisch Informationen über Ihre Repositories sammeln und die Ergebnisse als Markdown in einem Microagent speichern. Der Microagent hilft OpenHands, in zukünftigen Gesprächen schneller und genauer zu arbeiten.", - "uk": "OpenHands може автоматично вивчати ваші репозиторії та зберігати свої висновки у вигляді markdown у мікроагенті. Мікроагент допоможе OpenHands працювати швидше та точніше у майбутніх розмовах." + "uk": "OpenHands може автоматично вивчати ваші репозиторії та зберігати свої висновки у вигляді markdown у мікроагенті. Мікроагент допоможе OpenHands працювати швидше та точніше у майбутніх розмовах.", + "ca": "OpenHands pot aprendre automàticament sobre els vostres repositoris i desar els seus resultats com a markdown en un microagent. El microagent ajudarà OpenHands a funcionar més ràpidament i amb més precisió en futures converses." }, "MICROAGENT_MANAGEMENT$ADD_A_MICROAGENT_TO": { "en": "Add a Microagent to", @@ -12909,7 +13715,8 @@ "fr": "Ajouter un microagent à", "tr": "Bir mikro ajan ekle", "de": "Microagent hinzufügen zu", - "uk": "Додати мікроагента до" + "uk": "Додати мікроагента до", + "ca": "Afegeix un Microagent a" }, "MICROAGENT_MANAGEMENT$ADD_A_MICROAGENT": { "en": "Add a Microagent", @@ -12925,7 +13732,8 @@ "fr": "Ajouter un microagent", "tr": "Bir mikro ajan ekle", "de": "Microagent hinzufügen", - "uk": "Додати мікроагента" + "uk": "Додати мікроагента", + "ca": "Afegeix un Microagent" }, "MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT": { "en": "Update microagent", @@ -12941,7 +13749,8 @@ "fr": "Mettre à jour le microagent", "tr": "Mikro ajanı güncelle", "de": "Microagent aktualisieren", - "uk": "Оновити мікроагента" + "uk": "Оновити мікроагента", + "ca": "Actualitza el microagent" }, "MICROAGENT_MANAGEMENT$ADD_MICROAGENT_MODAL_DESCRIPTION": { "en": "OpenHands will create a new microagent based on your instructions.", @@ -12957,7 +13766,8 @@ "fr": "OpenHands créera un nouveau microagent selon vos instructions.", "tr": "OpenHands, talimatlarınıza göre yeni bir mikro ajan oluşturacaktır.", "de": "OpenHands erstellt einen neuen Microagenten basierend auf Ihren Anweisungen.", - "uk": "OpenHands створить нового мікроагента відповідно до ваших інструкцій." + "uk": "OpenHands створить нового мікроагента відповідно до ваших інструкцій.", + "ca": "OpenHands crearà un nou microagent basat en les vostres instruccions." }, "MICROAGENT_MANAGEMENT$WHAT_TO_DO": { "en": "What would you like the Microagent to do?", @@ -12973,7 +13783,8 @@ "fr": "Que souhaitez-vous que le microagent fasse ?", "tr": "Mikro ajanın ne yapmasını istersiniz?", "de": "Was soll der Microagent tun?", - "uk": "Що в,и хочете, щоб зробив мікроагент?" + "uk": "Що в,и хочете, щоб зробив мікроагент?", + "ca": "Què voleu que faci el Microagent?" }, "MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_DO": { "en": "Describe what you would like the Microagent to do.", @@ -12989,7 +13800,8 @@ "fr": "Décrivez ce que vous souhaitez que le microagent fasse.", "tr": "Mikro ajanın ne yapmasını istediğinizi açıklayın.", "de": "Beschreiben Sie, was der Microagent tun soll.", - "uk": "Опишіть, що ви хочете, щоб зробив мікроагент." + "uk": "Опишіть, що ви хочете, щоб зробив мікроагент.", + "ca": "Descriviu el que voleu que faci el Microagent." }, "MICROAGENT_MANAGEMENT$ADD_TRIGGERS": { "en": "Define triggers for the Microagent", @@ -13005,7 +13817,8 @@ "fr": "Définir des déclencheurs pour le microagent", "tr": "Mikro ajan için tetikleyiciler tanımlayın", "de": "Definieren Sie Auslöser für den Microagenten", - "uk": "Визначте тригери для мікроагента" + "uk": "Визначте тригери для мікроагента", + "ca": "Definiu disparadors per al Microagent" }, "MICROAGENT_MANAGEMENT$HELP_TEXT_DESCRIBING_VALID_TRIGGERS": { "en": "Enter a keyword that OpenHands will use to trigger this microagent (Optional).", @@ -13021,7 +13834,8 @@ "fr": "Entrez un mot-clé qu'OpenHands utilisera pour déclencher ce microagent (facultatif).", "tr": "OpenHands'ın bu mikro ajanı tetiklemek için kullanacağı bir anahtar kelime girin (İsteğe bağlı).", "de": "Geben Sie ein Schlüsselwort ein, das OpenHands verwendet, um diesen Microagenten auszulösen (optional).", - "uk": "Введіть ключове слово, яке OpenHands використовуватиме для запуску цього мікроагента (необов'язково)." + "uk": "Введіть ключове слово, яке OpenHands використовуватиме для запуску цього мікроагента (необов'язково).", + "ca": "Introduïu una paraula clau que OpenHands utilitzarà per activar aquest microagent (opcional)." }, "COMMON$FOR_EXAMPLE": { "en": "For example", @@ -13037,7 +13851,8 @@ "fr": "Par exemple", "tr": "Örneğin", "de": "Zum Beispiel", - "uk": "Наприклад:" + "uk": "Наприклад:", + "ca": "Per exemple" }, "COMMON$TEST_DB_MIGRATION": { "en": "Test DB Migration", @@ -13053,7 +13868,8 @@ "fr": "Tester la migration de la base de données", "tr": "Veritabanı geçişini test et", "de": "Datenbankmigration testen", - "uk": "Тестування міграції БД" + "uk": "Тестування міграції БД", + "ca": "Prova la migració de BD" }, "COMMON$RUN_TEST": { "en": "Run Test", @@ -13069,7 +13885,8 @@ "fr": "Exécuter le test", "tr": "Testi çalıştır", "de": "Test ausführen", - "uk": "Запустити тест" + "uk": "Запустити тест", + "ca": "Executa la prova" }, "COMMON$RUN_APP": { "en": "Run App", @@ -13085,7 +13902,8 @@ "fr": "Exécuter l'application", "tr": "Uygulamayı çalıştır", "de": "App ausführen", - "uk": "Запустити додаток" + "uk": "Запустити додаток", + "ca": "Executa l'aplicació" }, "COMMON$LEARN_FILE_STRUCTURE": { "en": "Learn File Structure", @@ -13101,7 +13919,8 @@ "fr": "Apprendre la structure des fichiers", "tr": "Dosya yapısını öğren", "de": "Dateistruktur lernen", - "uk": "Вивчити структуру файлів" + "uk": "Вивчити структуру файлів", + "ca": "Aprèn l'estructura de fitxers" }, "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_USER_LEVEL_MICROAGENTS": { "en": "You do not have user-level microagents", @@ -13117,7 +13936,8 @@ "fr": "Vous n'avez pas de microagents au niveau utilisateur", "tr": "Kullanıcı düzeyinde mikro ajanınız yok", "de": "Sie haben keine Mikroagenten auf Benutzerebene", - "uk": "У вас немає мікроагентів на рівні користувача" + "uk": "У вас немає мікроагентів на рівні користувача", + "ca": "No teniu microagents a nivell d'usuari" }, "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_MICROAGENTS": { "en": "You do not have microagents", @@ -13133,7 +13953,8 @@ "fr": "Vous n'avez pas de microagents", "tr": "Mikro ajanınız yok", "de": "Sie haben keine Mikroagenten", - "uk": "У вас немає мікроагентів" + "uk": "У вас немає мікроагентів", + "ca": "No teniu microagents" }, "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_ORGANIZATION_LEVEL_MICROAGENTS": { "en": "You do not have organization-level microagents", @@ -13149,7 +13970,8 @@ "fr": "Vous n'avez pas de microagents au niveau organisation", "tr": "Organizasyon düzeyinde mikro ajanınız yok", "de": "Sie haben keine Mikroagenten auf Organisationsebene", - "uk": "У вас немає мікроагентів на рівні організації" + "uk": "У вас немає мікроагентів на рівні організації", + "ca": "No teniu microagents a nivell d'organització" }, "COMMON$SEARCH_REPOSITORIES": { "en": "Search repositories", @@ -13165,7 +13987,8 @@ "fr": "Rechercher des dépôts", "tr": "Depo ara", "de": "Repositorys durchsuchen", - "uk": "Пошук репозиторіїв" + "uk": "Пошук репозиторіїв", + "ca": "Cerca repositoris" }, "COMMON$READY_FOR_REVIEW": { "en": "Ready for review", @@ -13181,7 +14004,8 @@ "fr": "Prêt pour la relecture", "tr": "İncelemeye hazır", "de": "Bereit zur Überprüfung", - "uk": "Готово до перегляду" + "uk": "Готово до перегляду", + "ca": "Preparat per a la revisió" }, "COMMON$COMPLETED": { "en": "Completed", @@ -13197,7 +14021,8 @@ "fr": "Terminé", "tr": "Tamamlandı", "de": "Abgeschlossen", - "uk": "Завершено" + "uk": "Завершено", + "ca": "Completat" }, "COMMON$COMPLETED_PARTIALLY": { "en": "Completed partially", @@ -13213,7 +14038,8 @@ "fr": "Partiellement terminé", "tr": "Kısmen tamamlandı", "de": "Teilweise abgeschlossen", - "uk": "Частково завершено" + "uk": "Частково завершено", + "ca": "Completat parcialment" }, "COMMON$STOPPED": { "en": "Stopped", @@ -13229,7 +14055,8 @@ "fr": "Arrêté", "tr": "Durduruldu", "de": "Gestoppt", - "uk": "Зупинено" + "uk": "Зупинено", + "ca": "Aturat" }, "COMMON$WORKING_ON_IT": { "en": "Working on it", @@ -13245,7 +14072,8 @@ "fr": "En cours", "tr": "Üzerinde çalışılıyor", "de": "Wird bearbeitet", - "uk": "В процесі виконання" + "uk": "В процесі виконання", + "ca": "Treballant-hi" }, "MICROAGENT_MANAGEMENT$WE_ARE_WORKING_ON_IT": { "en": "We're working on it! Once OpenHands is done investigating, you'll be able to review its pull request before merging your new microagent.", @@ -13261,7 +14089,8 @@ "fr": "Nous y travaillons ! Une fois qu'OpenHands aura terminé l'investigation, vous pourrez examiner sa pull request avant de fusionner votre nouveau microagent.", "tr": "Üzerinde çalışıyoruz! OpenHands incelemeyi bitirdiğinde, yeni mikro ajanınızı birleştirmeden önce pull request'i gözden geçirebileceksiniz.", "de": "Wir arbeiten daran! Sobald OpenHands die Untersuchung abgeschlossen hat, können Sie den Pull Request überprüfen, bevor Sie Ihren neuen Microagenten zusammenführen.", - "uk": "Ми працюємо над цим! Після завершення розслідування OpenHands ви зможете переглянути його pull request перед об'єднанням нового мікроагента." + "uk": "Ми працюємо над цим! Після завершення розслідування OpenHands ви зможете переглянути його pull request перед об'єднанням нового мікроагента.", + "ca": "Hi estem treballant! Un cop OpenHands hagi acabat d'investigar, podreu revisar la seva sol·licitud de canvis abans de fusionar el vostre nou microagent." }, "MICROAGENT_MANAGEMENT$YOUR_MICROAGENT_IS_READY": { "en": "Your microagent is ready! Merge the PR in GitHub to start using it.", @@ -13277,7 +14106,8 @@ "fr": "Votre micro-agent est prêt ! Fusionnez la PR sur GitHub pour commencer à l'utiliser.", "tr": "Mikro ajanınız hazır! Kullanmak için GitHub'da PR'ı birleştirin.", "de": "Ihr Microagent ist bereit! Führen Sie den PR in GitHub zusammen, um ihn zu verwenden.", - "uk": "Ваш мікроагент готовий! Злийте PR у GitHub, щоб почати ним користуватися." + "uk": "Ваш мікроагент готовий! Злийте PR у GitHub, щоб почати ним користуватися.", + "ca": "El vostre microagent està preparat! Fusioneu la PR a GitHub per començar a fer-lo servir." }, "COMMON$REVIEW_PR_IN": { "en": "Review PR in", @@ -13293,7 +14123,8 @@ "fr": "Examiner la PR sur", "tr": "PR'ı şurada gözden geçir:", "de": "PR überprüfen in", - "uk": "Переглянути PR у" + "uk": "Переглянути PR у", + "ca": "Revisa la PR a" }, "COMMON$EDIT_IN": { "en": "Edit in", @@ -13309,7 +14140,8 @@ "fr": "Modifier dans", "tr": "Şurada düzenle:", "de": "Bearbeiten in", - "uk": "Редагувати у" + "uk": "Редагувати у", + "ca": "Edita a" }, "COMMON$LEARN": { "en": "Learn", @@ -13325,7 +14157,8 @@ "fr": "Apprendre", "tr": "Öğren", "de": "Lernen", - "uk": "Вчитися" + "uk": "Вчитися", + "ca": "Aprèn" }, "COMMON$LEARN_SOMETHING_NEW": { "en": "Learn something new", @@ -13341,7 +14174,8 @@ "fr": "Apprendre quelque chose de nouveau", "tr": "Yeni bir şey öğren", "de": "Etwas Neues lernen", - "uk": "Вивчити щось нове" + "uk": "Вивчити щось нове", + "ca": "Aprèn alguna cosa nova" }, "COMMON$STARTING": { "en": "Starting", @@ -13357,7 +14191,8 @@ "fr": "Démarrage", "tr": "Başlatılıyor", "de": "Wird gestartet", - "uk": "Запуск" + "uk": "Запуск", + "ca": "Iniciant" }, "COMMON$STOPPING": { "en": "Stopping...", @@ -13373,7 +14208,8 @@ "fr": "Arrêt...", "tr": "Durduruluyor...", "de": "Wird gestoppt...", - "uk": "Зупинка..." + "uk": "Зупинка...", + "ca": "Aturant..." }, "MICROAGENT_MANAGEMENT$ERROR": { "en": "The system has encountered an error. Please try again later.", @@ -13389,7 +14225,8 @@ "fr": "Le système a rencontré une erreur. Veuillez réessayer plus tard.", "tr": "Sistem bir hata ile karşılaştı. Lütfen daha sonra tekrar deneyin.", "de": "Das System hat einen Fehler festgestellt. Bitte versuchen Sie es später erneut.", - "uk": "Система зіткнулася з помилкою. Будь ласка, спробуйте пізніше." + "uk": "Система зіткнулася з помилкою. Будь ласка, спробуйте пізніше.", + "ca": "El sistema ha trobat un error. Torneu-ho a intentar més tard." }, "MICROAGENT_MANAGEMENT$CONVERSATION_STOPPED": { "en": "The conversation has been stopped.", @@ -13405,7 +14242,8 @@ "fr": "La conversation a été arrêtée.", "tr": "Konuşma durduruldu.", "de": "Das Gespräch wurde gestoppt.", - "uk": "Розмову зупинено." + "uk": "Розмову зупинено.", + "ca": "La conversa s'ha aturat." }, "MICROAGENT_MANAGEMENT$LEARN_THIS_REPO_MODAL_TITLE": { "en": "Learn this repository?", @@ -13421,7 +14259,8 @@ "fr": "Apprendre ce dépôt ?", "tr": "Bu depoyu öğrenmek ister misiniz?", "de": "Dieses Repository lernen?", - "uk": "Вивчити цей репозиторій?" + "uk": "Вивчити цей репозиторій?", + "ca": "Aprendre d'aquest repositori?" }, "MICROAGENT_MANAGEMENT$LEARN_THIS_REPO_MODAL_DESCRIPTION": { "en": "Do you want OpenHands to launch a new conversation to help you understand this repository?", @@ -13437,7 +14276,8 @@ "fr": "Voulez-vous qu'OpenHands lance une nouvelle conversation pour vous aider à comprendre ce dépôt ?", "tr": "OpenHands'in bu depoyu anlamanıza yardımcı olacak yeni bir konuşma başlatmasını ister misiniz?", "de": "Möchten Sie, dass OpenHands eine neue Unterhaltung startet, um Ihnen dieses Repository zu erklären?", - "uk": "Бажаєте, щоб OpenHands розпочав нову розмову, щоб допомогти вам зрозуміти цей репозиторій?" + "uk": "Бажаєте, щоб OpenHands розпочав нову розмову, щоб допомогти вам зрозуміти цей репозиторій?", + "ca": "Voleu que OpenHands iniciï una nova conversa per ajudar-vos a entendre aquest repositori?" }, "MICROAGENT_MANAGEMENT$WHAT_YOU_WOULD_LIKE_TO_KNOW_ABOUT_THIS_REPO": { "en": "What would you like to know about this repository? (optional)", @@ -13453,7 +14293,8 @@ "fr": "Que souhaitez-vous savoir sur ce dépôt ? (facultatif)", "tr": "Bu depo hakkında ne bilmek istersiniz? (isteğe bağlı)", "de": "Was möchten Sie über dieses Repository wissen? (optional)", - "uk": "Що ви хотіли б дізнатися про цей репозиторій? (необов'язково)" + "uk": "Що ви хотіли б дізнатися про цей репозиторій? (необов'язково)", + "ca": "Què voleu saber sobre aquest repositori? (opcional)" }, "MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_KNOW_ABOUT_THIS_REPO": { "en": "Describe what you would like to know about this repository.", @@ -13469,7 +14310,8 @@ "fr": "Décrivez ce que vous souhaitez savoir sur ce dépôt.", "tr": "Bu depo hakkında ne bilmek istediğinizi açıklayın.", "de": "Beschreiben Sie, was Sie über dieses Repository wissen möchten.", - "uk": "Опишіть, що ви хотіли б дізнатися про цей репозиторій." + "uk": "Опишіть, що ви хотіли б дізнатися про цей репозиторій.", + "ca": "Descriviu el que voleu saber sobre aquest repositori." }, "MICROAGENT_MANAGEMENT$UPDATE_MICROAGENT_MODAL_DESCRIPTION": { "en": "OpenHands will update the microagent based on your instructions.", @@ -13485,7 +14327,8 @@ "fr": "OpenHands mettra à jour le microagent selon vos instructions.", "tr": "OpenHands, talimatlarınıza göre mikro ajanı güncelleyecektir.", "de": "OpenHands aktualisiert den Microagenten basierend auf Ihren Anweisungen.", - "uk": "OpenHands оновить мікроагента відповідно до ваших інструкцій." + "uk": "OpenHands оновить мікроагента відповідно до ваших інструкцій.", + "ca": "OpenHands actualitzarà el microagent basant-se en les vostres instruccions." }, "SETTINGS$GIT_USERNAME": { "en": "Git Username", @@ -13501,7 +14344,8 @@ "fr": "Nom d'utilisateur Git", "tr": "Git Kullanıcı Adı", "de": "Git-Benutzername", - "uk": "Ім'я користувача Git" + "uk": "Ім'я користувача Git", + "ca": "Nom d'usuari de Git" }, "SETTINGS$GIT_EMAIL": { "en": "Git Email", @@ -13517,7 +14361,8 @@ "fr": "Email Git", "tr": "Git E-posta", "de": "Git-E-Mail", - "uk": "Електронна пошта Git" + "uk": "Електронна пошта Git", + "ca": "Correu electrònic de Git" }, "PROJECT_MANAGEMENT$TITLE": { "en": "Project Management", @@ -13533,7 +14378,8 @@ "fr": "Gestion de projet", "tr": "Proje Yönetimi", "de": "Projektmanagement", - "uk": "Управління проектами" + "uk": "Управління проектами", + "ca": "Gestió de projectes" }, "PROJECT_MANAGEMENT$VALIDATE_INTEGRATION_ERROR": { "en": "Failed to validate integration. Please try again later.", @@ -13549,7 +14395,8 @@ "fr": "Échec de la validation de l'intégration. Veuillez réessayer ultérieurement.", "tr": "Entegrasyon doğrulanamadı. Lütfen daha sonra tekrar deneyin.", "de": "Die Integration konnte nicht validiert werden. Bitte versuchen Sie es später noch einmal.", - "uk": "Не вдалося перевірити інтеграцію. Спробуйте пізніше." + "uk": "Не вдалося перевірити інтеграцію. Спробуйте пізніше.", + "ca": "No s'ha pogut validar la integració. Torneu-ho a intentar més tard." }, "PROJECT_MANAGEMENT$LINK_BUTTON_LABEL": { "en": "Link", @@ -13565,7 +14412,8 @@ "fr": "Lien", "tr": "Bağlantı", "de": "Link", - "uk": "Посилання" + "uk": "Посилання", + "ca": "Vincula" }, "PROJECT_MANAGEMENT$UNLINK_BUTTON_LABEL": { "en": "Unlink", @@ -13581,7 +14429,8 @@ "fr": "Dissocier", "tr": "Bağlantıyı kaldır", "de": "Verknüpfung aufheben", - "uk": "Від’єднати" + "uk": "Від’єднати", + "ca": "Desvincula" }, "PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE": { "en": "Link Workspace", @@ -13597,7 +14446,8 @@ "fr": "Lier l'espace de travail", "tr": "Çalışma Alanını Bağla", "de": "Arbeitsbereich verknüpfen", - "uk": "Пов'язати робочу область" + "uk": "Пов'язати робочу область", + "ca": "Vincula l'espai de treball" }, "PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL": { "en": "Configure", @@ -13613,7 +14463,8 @@ "fr": "Configurer", "tr": "Yapılandır", "de": "Konfigurieren", - "uk": "Налаштувати" + "uk": "Налаштувати", + "ca": "Configura" }, "PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL": { "en": "Edit", @@ -13629,7 +14480,8 @@ "fr": "Modifier", "tr": "Düzenle", "de": "Bearbeiten", - "uk": "Редагувати" + "uk": "Редагувати", + "ca": "Edita" }, "PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL": { "en": "Update", @@ -13645,7 +14497,8 @@ "fr": "Mettre à jour", "tr": "Güncelle", "de": "Aktualisieren", - "uk": "Оновити" + "uk": "Оновити", + "ca": "Actualitza" }, "PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE": { "en": "Configure {{platform}} Integration", @@ -13661,7 +14514,8 @@ "fr": "Configurer l'intégration {{platform}}", "tr": "{{platform}} Entegrasyonunu Yapılandır", "de": "{{platform}}-Integration konfigurieren", - "uk": "Налаштувати інтеграцію {{platform}}" + "uk": "Налаштувати інтеграцію {{platform}}", + "ca": "Configura la integració de {{platform}}" }, "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1": { "en": "Important: Make sure the workspace integration for your target workspace is already configured. Check the documentation for more information.", @@ -13677,7 +14531,8 @@ "fr": "Important :Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la documentation pour plus d'informations.", "tr": "Önemli: Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için belgelere bakın.", "de": "Wichtig:Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der Dokumentation.", - "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації." + "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації.", + "ca": "Important: Assegureu-vos que la integració de l'espai de treball per al vostre espai de treball de destinació ja estigui configurada. Consulteu la documentació per obtenir més informació." }, "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2": { "en": "Important: Check the documentation for more information about configuring the workspace integration or updating an existing integration.", @@ -13693,7 +14548,8 @@ "fr": "Important : Consultez la documentation pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.", "tr": "Önemli: Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için belgelere bakın.", "de": "Wichtig: Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der Dokumentation.", - "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції." + "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції.", + "ca": "Important: Consulteu la documentació per obtenir més informació sobre la configuració de la integració de l'espai de treball o l'actualització d'una integració existent." }, "PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT": { "en": "Workspace name can be found in the browser URL when you're accessing a resource (eg: issue) in {{platform}}.", @@ -13709,7 +14565,8 @@ "fr": "Le nom de l'espace de travail peut être trouvé dans l'URL du navigateur lorsque vous accédez à une ressource (ex : issue) dans {{platform}}.", "tr": "Çalışma alanı adı, {{platform}}'da bir kaynağa (örn: sorun) erişirken tarayıcı URL'sinde bulunabilir.", "de": "Der Arbeitsbereichsname ist in der Browser-URL zu finden, wenn Sie auf eine Ressource (z.B.: Issue) in {{platform}} zugreifen.", - "uk": "Назву робочого простору можна знайти в URL браузера під час доступу до ресурсу (наприклад: проблема) в {{platform}}." + "uk": "Назву робочого простору можна знайти в URL браузера під час доступу до ресурсу (наприклад: проблема) в {{platform}}.", + "ca": "El nom de l'espai de treball es pot trobar a la URL del navegador quan accediu a un recurs (p. ex.: problema) a {{platform}}." }, "PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL": { "en": "Workspace Name", @@ -13725,7 +14582,8 @@ "fr": "Nom de l'espace de travail", "tr": "Çalışma Alanı Adı", "de": "Arbeitsbereichsname", - "uk": "Назва робочої області" + "uk": "Назва робочої області", + "ca": "Nom de l'espai de treball" }, "PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER": { "en": "yourcompany.atlassian.net", @@ -13741,7 +14599,8 @@ "fr": "yourcompany.atlassian.net", "tr": "yourcompany.atlassian.net", "de": "yourcompany.atlassian.net", - "uk": "yourcompany.atlassian.net" + "uk": "yourcompany.atlassian.net", + "ca": "yourcompany.atlassian.net" }, "PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER": { "en": "jira.yourcompany.com", @@ -13757,7 +14616,8 @@ "fr": "jira.yourcompany.com", "tr": "jira.yourcompany.com", "de": "jira.yourcompany.com", - "uk": "jira.yourcompany.com" + "uk": "jira.yourcompany.com", + "ca": "jira.yourcompany.com" }, "PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER": { "en": "yourcompany", @@ -13773,7 +14633,8 @@ "fr": "yourcompany", "tr": "yourcompany", "de": "yourcompany", - "uk": "yourcompany" + "uk": "yourcompany", + "ca": "yourcompany" }, "PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL": { "en": "Webhook Secret", @@ -13789,7 +14650,8 @@ "fr": "Secret du webhook", "tr": "Web Kancası Sırrı", "de": "Webhook-Geheimnis", - "uk": "Секрет вебхуку" + "uk": "Секрет вебхуку", + "ca": "Secret del webhook" }, "PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER": { "en": "whsec_abcd1234efgh5678ijkl", @@ -13805,7 +14667,8 @@ "fr": "whsec_abcd1234efgh5678ijkl", "tr": "whsec_abcd1234efgh5678ijkl", "de": "whsec_abcd1234efgh5678ijkl", - "uk": "whsec_abcd1234efgh5678ijkl" + "uk": "whsec_abcd1234efgh5678ijkl", + "ca": "whsec_abcd1234efgh5678ijkl" }, "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL": { "en": "Service Account Email", @@ -13821,7 +14684,8 @@ "fr": "E-mail du compte de service", "tr": "Hizmet Hesabı E-postası", "de": "E-Mail-Adresse des Dienstkontos", - "uk": "Електронна адреса облікового запису служби" + "uk": "Електронна адреса облікового запису служби", + "ca": "Correu electrònic del compte de servei" }, "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_PLACEHOLDER": { "en": "test@example.com", @@ -13837,7 +14701,8 @@ "fr": "test@example.com", "tr": "test@example.com", "de": "test@example.com", - "uk": "test@example.com" + "uk": "test@example.com", + "ca": "test@example.com" }, "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_LABEL": { "en": "Service Account API Key", @@ -13853,7 +14718,8 @@ "fr": "Clé API du compte de service", "tr": "Hizmet Hesabı API Anahtarı", "de": "API-Schlüssel des Dienstkontos", - "uk": "Ключ API облікового запису служби" + "uk": "Ключ API облікового запису служби", + "ca": "Clau d'API del compte de servei" }, "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER": { "en": "sk-1234567890abcdef...", @@ -13869,7 +14735,8 @@ "fr": "sk-1234567890abcdef...", "tr": "sk-1234567890abcdef...", "de": "sk-1234567890abcdef...", - "uk": "sk-1234567890abcdef..." + "uk": "sk-1234567890abcdef...", + "ca": "sk-1234567890abcdef..." }, "PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL": { "en": "Connect", @@ -13885,7 +14752,8 @@ "fr": "Connecter", "tr": "Bağlamak", "de": "Verbinden", - "uk": "Підключитися" + "uk": "Підключитися", + "ca": "Connecta" }, "PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL": { "en": "Active", @@ -13901,7 +14769,8 @@ "fr": "Actif", "tr": "Aktif", "de": "Aktiv", - "uk": "Активний" + "uk": "Активний", + "ca": "Actiu" }, "PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR": { "en": "Workspace name can only contain letters, numbers, hyphens, and underscores", @@ -13917,7 +14786,8 @@ "fr": "Le nom de l'espace de travail ne peut contenir que des lettres, des chiffres, des tirets et des traits de soulignement", "tr": "Çalışma alanı adı yalnızca harfler, sayılar, tire ve alt çizgi içerebilir", "de": "Der Arbeitsbereichsname darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten", - "uk": "Назва робочого простору може містити тільки літери, цифри, дефіси та підкреслення" + "uk": "Назва робочого простору може містити тільки літери, цифри, дефіси та підкреслення", + "ca": "El nom de l'espai de treball només pot contenir lletres, números, guions i guions baixos" }, "PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR": { "en": "Webhook secret cannot contain spaces", @@ -13933,7 +14803,8 @@ "fr": "Le secret du webhook ne peut pas contenir d'espaces", "tr": "Webhook sırrı boşluk içeremez", "de": "Das Webhook-Geheimnis darf keine Leerzeichen enthalten", - "uk": "Секрет веб-хука не може містити пробіли" + "uk": "Секрет веб-хука не може містити пробіли", + "ca": "El secret del webhook no pot contenir espais" }, "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR": { "en": "Please enter a valid email address", @@ -13949,7 +14820,8 @@ "fr": "Veuillez saisir une adresse e-mail valide", "tr": "Lütfen geçerli bir e-posta adresi girin", "de": "Bitte geben Sie eine gültige E-Mail-Adresse ein", - "uk": "Будь ласка, введіть дійсну адресу електронної пошти" + "uk": "Будь ласка, введіть дійсну адресу електронної пошти", + "ca": "Introduïu una adreça de correu electrònic vàlida" }, "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR": { "en": "API key cannot contain spaces", @@ -13965,7 +14837,8 @@ "fr": "La clé API ne peut pas contenir d'espaces", "tr": "API anahtarı boşluk içeremez", "de": "Der API-Schlüssel darf keine Leerzeichen enthalten", - "uk": "Ключ API не може містити пробіли" + "uk": "Ключ API не може містити пробіли", + "ca": "La clau d'API no pot contenir espais" }, "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT": { "en": "Error loading microagent content.", @@ -13981,7 +14854,8 @@ "fr": "Erreur lors du chargement du contenu du microagent.", "tr": "Mikro ajan içeriği yüklenirken hata oluştu.", "de": "Fehler beim Laden des Microagent-Inhalts.", - "uk": "Помилка під час завантаження вмісту мікроагента." + "uk": "Помилка під час завантаження вмісту мікроагента.", + "ca": "Error en carregar el contingut del microagent." }, "SETTINGS$MCP_SERVER_TYPE_SSE": { "en": "SSE", @@ -13997,7 +14871,8 @@ "fr": "SSE", "tr": "SSE", "de": "SSE", - "uk": "SSE" + "uk": "SSE", + "ca": "SSE" }, "SETTINGS$MCP_SERVER_TYPE_STDIO": { "en": "STDIO", @@ -14013,7 +14888,8 @@ "fr": "STDIO", "tr": "STDIO", "de": "STDIO", - "uk": "STDIO" + "uk": "STDIO", + "ca": "STDIO" }, "SETTINGS$MCP_SERVER_TYPE_SHTTP": { "en": "SHTTP", @@ -14029,7 +14905,8 @@ "fr": "SHTTP", "tr": "SHTTP", "de": "SHTTP", - "uk": "SHTTP" + "uk": "SHTTP", + "ca": "SHTTP" }, "SETTINGS$MCP_ERROR_URL_REQUIRED": { "en": "URL is required", @@ -14045,7 +14922,8 @@ "fr": "L'URL est requise", "tr": "URL gereklidir", "de": "URL ist erforderlich", - "uk": "URL обов'язковий" + "uk": "URL обов'язковий", + "ca": "La URL és obligatòria" }, "SETTINGS$MCP_ERROR_URL_INVALID_PROTOCOL": { "en": "URL must use http:// or https://", @@ -14061,7 +14939,8 @@ "fr": "L'URL doit utiliser http:// ou https://", "tr": "URL http:// veya https:// kullanmalıdır", "de": "URL muss http:// oder https:// verwenden", - "uk": "URL повинен використовувати http:// або https://" + "uk": "URL повинен використовувати http:// або https://", + "ca": "La URL ha de fer servir http:// o https://" }, "SETTINGS$MCP_ERROR_URL_INVALID": { "en": "Invalid URL format", @@ -14077,7 +14956,8 @@ "fr": "Format d'URL invalide", "tr": "Geçersiz URL formatı", "de": "Ungültiges URL-Format", - "uk": "Недійсний формат URL" + "uk": "Недійсний формат URL", + "ca": "Format de URL no vàlid" }, "SETTINGS$MCP_ERROR_NAME_REQUIRED": { "en": "Name is required", @@ -14093,7 +14973,8 @@ "fr": "Le nom est requis", "tr": "Ad gereklidir", "de": "Name ist erforderlich", - "uk": "Ім'я обов'язкове" + "uk": "Ім'я обов'язкове", + "ca": "El nom és obligatori" }, "SETTINGS$MCP_ERROR_NAME_INVALID": { "en": "Name can only contain letters, numbers, hyphens, and underscores", @@ -14109,7 +14990,8 @@ "fr": "Le nom ne peut contenir que des lettres, des chiffres, des tirets et des traits de soulignement", "tr": "Ad yalnızca harf, rakam, tire ve alt çizgi içerebilir", "de": "Name darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten", - "uk": "Ім'я може містити лише літери, цифри, дефіси та підкреслення" + "uk": "Ім'я може містити лише літери, цифри, дефіси та підкреслення", + "ca": "El nom només pot contenir lletres, números, guions i guions baixos" }, "SETTINGS$MCP_ERROR_NAME_DUPLICATE": { "en": "A STDIO server with this name already exists", @@ -14125,7 +15007,8 @@ "fr": "Un serveur STDIO avec ce nom existe déjà", "tr": "Bu adda bir STDIO sunucusu zaten mevcut", "de": "Ein STDIO-Server mit diesem Namen existiert bereits", - "uk": "STDIO сервер з цим ім'ям вже існує" + "uk": "STDIO сервер з цим ім'ям вже існує", + "ca": "Ja existeix un servidor STDIO amb aquest nom" }, "SETTINGS$MCP_ERROR_COMMAND_REQUIRED": { "en": "Command is required", @@ -14141,7 +15024,8 @@ "fr": "La commande est requise", "tr": "Komut gereklidir", "de": "Befehl ist erforderlich", - "uk": "Команда обов'язкова" + "uk": "Команда обов'язкова", + "ca": "La comanda és obligatòria" }, "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES": { "en": "Command cannot contain spaces", @@ -14157,7 +15041,8 @@ "fr": "La commande ne peut pas contenir d'espaces", "tr": "Komut boşluk içeremez", "de": "Befehl darf keine Leerzeichen enthalten", - "uk": "Команда не може містити пробіли" + "uk": "Команда не може містити пробіли", + "ca": "La comanda no pot contenir espais" }, "SETTINGS$MCP_ERROR_URL_DUPLICATE": { "en": "A server with this URL already exists for the selected type", @@ -14173,7 +15058,8 @@ "fr": "A server with this URL already exists for the selected type", "tr": "A server with this URL already exists for the selected type", "de": "A server with this URL already exists for the selected type", - "uk": "A server with this URL already exists for the selected type" + "uk": "A server with this URL already exists for the selected type", + "ca": "Ja existeix un servidor amb aquesta URL per al tipus seleccionat" }, "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": { "en": "Environment variables must follow KEY=value format", @@ -14189,7 +15075,8 @@ "fr": "Environment variables must follow KEY=value format", "tr": "Environment variables must follow KEY=value format", "de": "Environment variables must follow KEY=value format", - "uk": "Environment variables must follow KEY=value format" + "uk": "Environment variables must follow KEY=value format", + "ca": "Les variables d'entorn han de seguir el format CLAU=valor" }, "SETTINGS$MCP_ERROR_TIMEOUT_INVALID_NUMBER": { "en": "Timeout must be a valid number", @@ -14205,7 +15092,8 @@ "fr": "Le timeout doit être un nombre valide", "tr": "Zaman aşımı geçerli bir sayı olmalıdır", "de": "Timeout muss eine gültige Zahl sein", - "uk": "Таймаут повинен бути дійсним числом" + "uk": "Таймаут повинен бути дійсним числом", + "ca": "El temps d'espera ha de ser un número vàlid" }, "SETTINGS$MCP_ERROR_TIMEOUT_POSITIVE": { "en": "Timeout must be positive", @@ -14221,7 +15109,8 @@ "fr": "Le timeout doit être positif", "tr": "Zaman aşımı pozitif olmalıdır", "de": "Timeout muss positiv sein", - "uk": "Таймаут повинен бути позитивним" + "uk": "Таймаут повинен бути позитивним", + "ca": "El temps d'espera ha de ser positiu" }, "SETTINGS$MCP_ERROR_TIMEOUT_MAX_EXCEEDED": { "en": "Timeout cannot exceed 3600 seconds (1 hour)", @@ -14237,7 +15126,8 @@ "fr": "Le timeout ne peut pas dépasser 3600 secondes (1 heure)", "tr": "Zaman aşımı 3600 saniyeyi (1 saat) aşamaz", "de": "Timeout kann 3600 Sekunden (1 Stunde) nicht überschreiten", - "uk": "Таймаут не може перевищувати 3600 секунд (1 година)" + "uk": "Таймаут не може перевищувати 3600 секунд (1 година)", + "ca": "El temps d'espera no pot superar els 3600 segons (1 hora)" }, "SETTINGS$MCP_SERVER_TYPE": { "en": "Server Type", @@ -14253,7 +15143,8 @@ "fr": "Type de serveur", "tr": "Sunucu türü", "de": "Server-Typ", - "uk": "Тип сервера" + "uk": "Тип сервера", + "ca": "Tipus de servidor" }, "SETTINGS$MCP_API_KEY_PLACEHOLDER": { "en": "Enter API key (optional)", @@ -14269,7 +15160,8 @@ "fr": "Saisir la clé API (optionnel)", "tr": "API anahtarını girin (isteğe bağlı)", "de": "API-Schlüssel eingeben (optional)", - "uk": "Введіть API ключ (необов'язково)" + "uk": "Введіть API ключ (необов'язково)", + "ca": "Introduïu la clau d'API (opcional)" }, "SETTINGS$MCP_COMMAND_ARGUMENTS": { "en": "Command Arguments", @@ -14285,7 +15177,8 @@ "fr": "Arguments de commande", "tr": "Komut argümanları", "de": "Befehlsargumente", - "uk": "Аргументи команди" + "uk": "Аргументи команди", + "ca": "Arguments de la comanda" }, "SETTINGS$MCP_COMMAND_ARGUMENTS_HELP": { "en": "Enter each argument on a separate line. Arguments will be passed to the command in the order listed.", @@ -14301,7 +15194,8 @@ "fr": "Entrez chaque argument sur une ligne séparée. Les arguments seront passés à la commande dans l'ordre listé.", "tr": "Her argümanı ayrı bir satıra girin. Argümanlar listedeki sırayla komuta aktarılacaktır.", "de": "Geben Sie jedes Argument in einer separaten Zeile ein. Die Argumente werden in der aufgelisteten Reihenfolge an den Befehl übergeben.", - "uk": "Введіть кожен аргument в окремому рядку. Аргументи будуть передані команді в порядку, в якому вони перераховані." + "uk": "Введіть кожен аргument в окремому рядку. Аргументи будуть передані команді в порядку, в якому вони перераховані.", + "ca": "Introduïu cada argument en una línia separada. Els arguments es passaran a la comanda en l'ordre indicat." }, "SETTINGS$MCP_ENVIRONMENT_VARIABLES": { "en": "Environment Variables", @@ -14317,7 +15211,8 @@ "fr": "Variables d'environnement", "tr": "Ortam değişkenleri", "de": "Umgebungsvariablen", - "uk": "Змінні середовища" + "uk": "Змінні середовища", + "ca": "Variables d'entorn" }, "SETTINGS$MCP_ADD_SERVER": { "en": "Add Server", @@ -14333,7 +15228,8 @@ "fr": "Ajouter un serveur", "tr": "Sunucu ekle", "de": "Server hinzufügen", - "uk": "Додати сервер" + "uk": "Додати сервер", + "ca": "Afegeix un servidor" }, "SETTINGS$MCP_SAVE_SERVER": { "en": "Save Server", @@ -14349,7 +15245,8 @@ "fr": "Enregistrer le serveur", "tr": "Sunucuyu kaydet", "de": "Server speichern", - "uk": "Зберегти сервер" + "uk": "Зберегти сервер", + "ca": "Desa el servidor" }, "SETTINGS$MCP_NO_SERVERS": { "en": "No servers configured", @@ -14365,7 +15262,8 @@ "fr": "Aucun serveur configuré", "tr": "Yapılandırılmış sunucu yok", "de": "Keine Server konfiguriert", - "uk": "Сервери не налаштовані" + "uk": "Сервери не налаштовані", + "ca": "No hi ha servidors configurats" }, "SETTINGS$MCP_SERVER_DETAILS": { "en": "Server Details", @@ -14381,7 +15279,8 @@ "fr": "Détails du serveur", "tr": "Sunucu detayları", "de": "Server-Details", - "uk": "Деталі сервера" + "uk": "Деталі сервера", + "ca": "Detalls del servidor" }, "SETTINGS$MCP_CONFIRM_DELETE": { "en": "Are you sure you want to delete this server?", @@ -14397,7 +15296,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer ce serveur ?", "tr": "Bu sunucuyu silmek istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie diesen Server löschen möchten?", - "uk": "Ви впевнені, що хочете видалити цей сервер?" + "uk": "Ви впевнені, що хочете видалити цей сервер?", + "ca": "Esteu segur que voleu eliminar aquest servidor?" }, "SETTINGS$MCP_CONFIRM_CHANGES": { "en": "Confirm Changes", @@ -14413,7 +15313,8 @@ "fr": "Confirmer les modifications", "tr": "Değişiklikleri Onayla", "de": "Änderungen bestätigen", - "uk": "Підтвердити зміни" + "uk": "Підтвердити зміни", + "ca": "Confirma els canvis" }, "SETTINGS$MCP_DEFAULT_CONFIG": { "en": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", @@ -14429,7 +15330,8 @@ "fr": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", "tr": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", "de": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", - "uk": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}" + "uk": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}", + "ca": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}" }, "PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER": { "en": "myworkspace", @@ -14445,7 +15347,8 @@ "fr": "monworkspace", "tr": "benimworkspace", "de": "meinarbeitsbereich", - "uk": "моя-робоча-область" + "uk": "моя-робоча-область", + "ca": "myworkspace" }, "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION": { "en": "Important: Check the documentation for more information about configuring the workspace integration or updating an existing integration.", @@ -14461,7 +15364,8 @@ "fr": "Important : Consultez la documentation pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.", "tr": "Önemli: Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için belgelere bakın.", "de": "Wichtig: Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der Dokumentation.", - "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції." + "uk": "Важливо: Перегляньте документацію для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції.", + "ca": "Important: Consulteu la documentació per obtenir més informació sobre la configuració de la integració de l'espai de treball o l'actualització d'una integració existent." }, "PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION": { "en": "Important: Make sure the workspace integration for your target workspace is already configured. Check the documentation for more information.", @@ -14477,7 +15381,8 @@ "fr": "Important :Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la documentation pour plus d'informations.", "tr": "Önemli: Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için belgelere bakın.", "de": "Wichtig:Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der Dokumentation.", - "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації." + "uk": "Важливо: Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте документацію для отримання додаткової інформації.", + "ca": "Important: Assegureu-vos que la integració de l'espai de treball per al vostre espai de treball de destinació ja estigui configurada. Consulteu la documentació per obtenir més informació." }, "SETTINGS": { "en": "Environment variables must follow KEY=value format", @@ -14493,7 +15398,8 @@ "fr": "A server with this URL already exists for the selected type", "tr": "A server with this URL already exists for the selected type", "de": "A server with this URL already exists for the selected type", - "uk": "A server with this URL already exists for the selected type" + "uk": "A server with this URL already exists for the selected type", + "ca": "Les variables d'entorn han de seguir el format CLAU=valor" }, "MICROAGENT_MANAGEMENT$OPENING_PR_TO_CREATE_MICROAGENT": { "en": "Opening a PR to create the microagent for you...", @@ -14509,7 +15415,8 @@ "fr": "Ouverture d'une PR pour créer le microagent pour vous...", "tr": "Sizin için mikro ajanı oluşturmak üzere bir PR açılıyor...", "de": "Es wird ein PR geöffnet, um den Microagent für Sie zu erstellen...", - "uk": "Відкривається PR для створення мікроагента для вас..." + "uk": "Відкривається PR для створення мікроагента для вас...", + "ca": "Obrint una PR per crear el microagent per a vosaltres..." }, "MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW": { "en": "PR is ready for review! The microagent has been created successfully.", @@ -14525,7 +15432,8 @@ "fr": "La PR est prête pour révision ! Le microagent a été créé avec succès.", "tr": "PR incelemeye hazır! Mikro ajan başarıyla oluşturuldu.", "de": "PR ist bereit zur Überprüfung! Der Microagent wurde erfolgreich erstellt.", - "uk": "PR готовий до перегляду! Мікроагента успішно створено." + "uk": "PR готовий до перегляду! Мікроагента успішно створено.", + "ca": "La PR està preparada per a la revisió! El microagent s'ha creat correctament." }, "MICROAGENT_MANAGEMENT$PR_NOT_CREATED": { "en": "The agent has finished its task but was unable to create a PR.", @@ -14541,7 +15449,8 @@ "fr": "L'agent a terminé sa tâche mais n'a pas pu créer de PR.", "tr": "Ajan görevini tamamladı ancak bir PR oluşturamadı.", "de": "Der Agent hat seine Aufgabe abgeschlossen, konnte aber keinen PR erstellen.", - "uk": "Агент завершив завдання, але не зміг створити PR." + "uk": "Агент завершив завдання, але не зміг створити PR.", + "ca": "L'agent ha acabat la seva tasca però no ha pogut crear una PR." }, "MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT": { "en": "Something went wrong. Try initiating the microagent again.", @@ -14557,7 +15466,8 @@ "fr": "Une erreur s'est produite. Essayez de relancer le microagent.", "tr": "Bir şeyler ters gitti. Mikro ajanı tekrar başlatmayı deneyin.", "de": "Etwas ist schiefgelaufen. Versuchen Sie, den Microagenten erneut zu starten.", - "uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз." + "uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз.", + "ca": "Alguna cosa ha anat malament. Intenteu iniciar el microagent de nou." }, "MICROAGENT$STATUS_WAITING": { "en": "Waiting for runtime to start...", @@ -14573,7 +15483,8 @@ "fr": "En attente du démarrage du runtime...", "tr": "Çalışma zamanının başlaması bekleniyor...", "de": "Warten auf den Start der Laufzeit...", - "uk": "Очікування запуску середовища виконання..." + "uk": "Очікування запуску середовища виконання...", + "ca": "Esperant que l'entorn d'execució s'iniciï..." }, "MICROAGENT$UNKNOWN_ERROR": { "en": "Unknown error, please try again", @@ -14589,7 +15500,8 @@ "fr": "Erreur inconnue, veuillez réessayer", "tr": "Bilinmeyen hata, lütfen tekrar deneyin", "de": "Unbekannter Fehler, bitte versuchen Sie es erneut", - "uk": "Невідома помилка, спробуйте ще раз" + "uk": "Невідома помилка, спробуйте ще раз", + "ca": "Error desconegut, torneu-ho a intentar" }, "MICROAGENT$CONVERSATION_STARTING": { "en": "Starting conversation...", @@ -14605,7 +15517,8 @@ "fr": "Démarrage de la conversation...", "tr": "Konuşma başlatılıyor...", "de": "Gespräch wird gestartet...", - "uk": "Розпочинається розмова..." + "uk": "Розпочинається розмова...", + "ca": "Iniciant la conversa..." }, "MICROAGENT_MANAGEMENT$EXISTING_MICROAGENTS": { "en": "Existing Microagents", @@ -14621,7 +15534,8 @@ "fr": "Microagents existants", "tr": "Mevcut Mikroajanlar", "de": "Vorhandene Mikroagenten", - "uk": "Існуючі мікроагенти" + "uk": "Існуючі мікроагенти", + "ca": "Microagents existents" }, "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT": { "en": "LLM Analyzer (Default)", @@ -14637,7 +15551,8 @@ "fr": "Analyseur LLM (Par défaut)", "tr": "LLM Analizörü (Varsayılan)", "de": "LLM-Analysator (Standard)", - "uk": "Аналізатор LLM (За замовчуванням)" + "uk": "Аналізатор LLM (За замовчуванням)", + "ca": "Analitzador LLM (per defecte)" }, "SETTINGS$SECURITY_ANALYZER_NONE": { "en": "None (Ask for every command)", @@ -14653,7 +15568,8 @@ "fr": "Aucun (Demander pour chaque commande)", "tr": "Yok (Her komutta sor)", "de": "Keine (Bei jedem Befehl nachfragen)", - "uk": "Немає (Запитувати для кожної команди)" + "uk": "Немає (Запитувати для кожної команди)", + "ca": "Cap (demana per cada comanda)" }, "SETTINGS$SECURITY_ANALYZER_INVARIANT": { "en": "Invariant Rule-based Analyzer", @@ -14669,7 +15585,8 @@ "fr": "Analyseur à base de règles Invariant", "tr": "Invariant Kural Tabanlı Analizör", "de": "Invariant regelbasierter Analysator", - "uk": "Аналізатор на основі правил Invariant" + "uk": "Аналізатор на основі правил Invariant", + "ca": "Analitzador basat en regles Invariant" }, "COMMON$HIGH_RISK": { "en": "High Risk", @@ -14685,7 +15602,8 @@ "fr": "Risque élevé", "tr": "Yüksek Risk", "de": "Hohes Risiko", - "uk": "Високий ризик" + "uk": "Високий ризик", + "ca": "Alt risc" }, "COMMON$SEE": { "en": "see", @@ -14701,7 +15619,8 @@ "fr": "voir", "tr": "bak", "de": "siehe", - "uk": "переглянути" + "uk": "переглянути", + "ca": "consulteu" }, "COMMON$ADVANCED_SETTINGS": { "en": "advanced settings", @@ -14717,7 +15636,8 @@ "fr": "paramètres avancés", "tr": "gelişmiş ayarlar", "de": "erweiterte Einstellungen", - "uk": "розширені налаштування" + "uk": "розширені налаштування", + "ca": "configuració avançada" }, "SECURITY_ANALYZER$UNKNOWN_RISK": { "en": "Unknown Risk", @@ -14733,7 +15653,8 @@ "fr": "Risque inconnu", "tr": "Bilinmeyen risk", "ja": "不明なリスク", - "uk": "Невідомий ризик" + "uk": "Невідомий ризик", + "ca": "Risc desconegut" }, "SECURITY_ANALYZER$LOW_RISK": { "en": "Low Risk", @@ -14749,7 +15670,8 @@ "fr": "Risque faible", "tr": "Düşük risk", "ja": "低リスク", - "uk": "Низький ризик" + "uk": "Низький ризик", + "ca": "Risc baix" }, "SECURITY_ANALYZER$MEDIUM_RISK": { "en": "Medium Risk", @@ -14765,7 +15687,8 @@ "fr": "Risque moyen", "tr": "Orta risk", "ja": "中リスク", - "uk": "Середній ризик" + "uk": "Середній ризик", + "ca": "Risc mitjà" }, "SECURITY_ANALYZER$HIGH_RISK": { "en": "High Risk", @@ -14781,7 +15704,8 @@ "fr": "Risque élevé", "tr": "Yüksek risk", "ja": "高リスク", - "uk": "Високий ризик" + "uk": "Високий ризик", + "ca": "Risc alt" }, "ACTION$CONFIRM_DELETE": { "en": "Confirm Delete", @@ -14797,7 +15721,8 @@ "ar": "تأكيد الحذف", "no": "Bekreft sletting", "tr": "Silme Onayı", - "uk": "Підтвердити видалення" + "uk": "Підтвердити видалення", + "ca": "Confirma l'eliminació" }, "ACTION$CONFIRM_STOP": { "en": "Confirm Stop", @@ -14813,7 +15738,8 @@ "ar": "تأكيد الإيقاف", "no": "Bekreft stopp", "tr": "Durdurmayı Onayla", - "uk": "Підтвердити зупинку" + "uk": "Підтвердити зупинку", + "ca": "Confirma l'aturada" }, "ACTION$CONFIRM_CLOSE": { "en": "Confirm Close", @@ -14829,7 +15755,8 @@ "ar": "تأكيد الإغلاق", "no": "Bekreft lukking", "tr": "Kapatmayı Onayla", - "uk": "Підтвердити закриття" + "uk": "Підтвердити закриття", + "ca": "Confirma el tancament" }, "ACTION$CONFIRM_UPDATE": { "en": "Confirm Update", @@ -14845,7 +15772,8 @@ "ar": "تأكيد التحديث", "no": "Bekreft oppdatering", "tr": "Güncellemeyi Onayla", - "uk": "Підтвердити оновлення" + "uk": "Підтвердити оновлення", + "ca": "Confirma l'actualització" }, "AGENT_STATUS$AGENT_STOPPED": { "en": "Agent stopped", @@ -14861,7 +15789,8 @@ "fr": "Agent arrêté.", "tr": "Ajan durdu.", "de": "Agent gestoppt.", - "uk": "Агент зупинився." + "uk": "Агент зупинився.", + "ca": "Agent aturat" }, "AGENT_STATUS$ERROR_OCCURRED": { "en": "Error occurred", @@ -14877,7 +15806,8 @@ "fr": "Une erreur s'est produite.", "tr": "Hata oluştu.", "de": "Ein Fehler ist aufgetreten.", - "uk": "Сталася помилка." + "uk": "Сталася помилка.", + "ca": "S'ha produït un error" }, "AGENT_STATUS$INITIALIZING": { "en": "Initializing agent...", @@ -14893,7 +15823,8 @@ "fr": "Initialisation de l'agent.", "tr": "Ajan başlatılıyor.", "de": "Agent wird initialisiert.", - "uk": "Ініціалізація агента." + "uk": "Ініціалізація агента.", + "ca": "Inicialitzant l'agent..." }, "AGENT_STATUS$RUNNING_TASK": { "en": "Running task", @@ -14909,7 +15840,8 @@ "fr": "Exécution de la tâche.", "tr": "Görev yürütülüyor.", "de": "Aufgabe wird ausgeführt.", - "uk": "Виконує завдання." + "uk": "Виконує завдання.", + "ca": "Executant la tasca" }, "AGENT_STATUS$WAITING_FOR_TASK": { "en": "Waiting for task", @@ -14925,7 +15857,8 @@ "fr": "En attente de tâche.", "tr": "Görev bekleniyor.", "de": "Warten auf Aufgabe.", - "uk": "Очікує завдання." + "uk": "Очікує завдання.", + "ca": "Esperant una tasca" }, "BUTTON$DELETE_CONVERSATION": { "en": "Delete Conversation", @@ -14941,7 +15874,8 @@ "it": "Elimina conversazione", "pt": "Excluir conversa", "es": "Eliminar conversación", - "tr": "Sohbeti sil" + "tr": "Sohbeti sil", + "ca": "Elimina la conversa" }, "BUTTON$RENAME": { "en": "Rename", @@ -14957,7 +15891,8 @@ "fr": "Renommer", "tr": "Yeniden adlandır", "de": "Umbenennen", - "uk": "Перейменувати" + "uk": "Перейменувати", + "ca": "Canvia el nom" }, "COMMON$APP": { "en": "App", @@ -14973,7 +15908,8 @@ "fr": "Application", "tr": "Uygulama", "de": "App", - "uk": "Додаток" + "uk": "Додаток", + "ca": "Aplicació" }, "COMMON$PLANNER": { "en": "Planner", @@ -14989,7 +15925,8 @@ "fr": "Planificateur", "tr": "Planlayıcı", "de": "Planer", - "uk": "Планувальник" + "uk": "Планувальник", + "ca": "Planificador" }, "COMMON$APPLICATION_SETTINGS": { "en": "Application Settings", @@ -15005,7 +15942,8 @@ "fr": "Paramètres de l'application", "tr": "Uygulama Ayarları", "de": "Anwendungseinstellungen", - "uk": "Налаштування застосунку" + "uk": "Налаштування застосунку", + "ca": "Configuració de l'aplicació" }, "COMMON$BROWSER": { "en": "Browser", @@ -15021,7 +15959,8 @@ "fr": "Navigateur", "tr": "Tarayıcı", "de": "Browser", - "uk": "Браузер" + "uk": "Браузер", + "ca": "Navegador" }, "COMMON$CHANGES": { "en": "Changes", @@ -15037,7 +15976,8 @@ "fr": "Modifications", "tr": "Değişiklikler", "de": "Änderungen", - "uk": "Зміни" + "uk": "Зміни", + "ca": "Canvis" }, "COMMON$CLICK_HERE": { "en": "Click here", @@ -15053,7 +15993,8 @@ "fr": "Cliquez ici", "tr": "Buraya tıklayın", "de": "Hier klicken", - "uk": "Натисніть тут" + "uk": "Натисніть тут", + "ca": "Feu clic aquí" }, "COMMON$CLOSE_CONVERSATION_STOP_RUNTIME": { "en": "Close Conversation (Stop Sandbox)", @@ -15069,7 +16010,8 @@ "fr": "Fermer la conversation (Arrêter le sandbox)", "tr": "Konuşmayı Kapat (Sandbox'ı Durdur)", "de": "Gespräch schließen (Sandbox beenden)", - "uk": "Закрити розмову (зупинити пісочницю)" + "uk": "Закрити розмову (зупинити пісочницю)", + "ca": "Tanca la conversa (atura l'entorn d'execució)" }, "COMMON$CODE": { "en": "Code", @@ -15085,7 +16027,8 @@ "fr": "Code", "tr": "Kod", "de": "Code", - "uk": "Код" + "uk": "Код", + "ca": "Codi" }, "COMMON$CREATE_NEW_BRANCH": { "en": "Create New Branch", @@ -15101,7 +16044,8 @@ "fr": "Créer une nouvelle branche", "tr": "Yeni Dal Oluştur", "de": "Neuen Branch erstellen", - "uk": "Створити нову гілку" + "uk": "Створити нову гілку", + "ca": "Crea una nova branca" }, "COMMON$CREATE_PR": { "en": "Create PR", @@ -15117,7 +16061,8 @@ "fr": "Créer une PR", "tr": "PR Oluştur", "de": "PR erstellen", - "uk": "Створити PR" + "uk": "Створити PR", + "ca": "Crea una PR" }, "COMMON$DELETE_CONVERSATION": { "en": "Delete Conversation", @@ -15133,7 +16078,8 @@ "fr": "Supprimer la conversation", "tr": "Konuşmayı Sil", "de": "Gespräch löschen", - "uk": "Видалити розмову" + "uk": "Видалити розмову", + "ca": "Elimina la conversa" }, "COMMON$DROP_YOUR_FILES_HERE": { "en": "Drop your files here", @@ -15149,7 +16095,8 @@ "fr": "Déposez vos fichiers ici", "tr": "Dosyalarınızı buraya bırakın", "de": "Legen Sie Ihre Dateien hier ab", - "uk": "Перетягніть ваші файли сюди" + "uk": "Перетягніть ваші файли сюди", + "ca": "Deixeu els fitxers aquí" }, "COMMON$ERROR": { "en": "Error", @@ -15165,7 +16112,8 @@ "fr": "Erreur", "tr": "Hata", "de": "Fehler", - "uk": "Помилка" + "uk": "Помилка", + "ca": "Error" }, "COMMON$GIT_PULL": { "en": "Git Pull", @@ -15181,7 +16129,8 @@ "fr": "Git Pull", "tr": "Git Pull", "de": "Git Pull", - "uk": "Git Pull" + "uk": "Git Pull", + "ca": "Git Pull" }, "COMMON$GIT_PUSH": { "en": "Git Push", @@ -15197,7 +16146,8 @@ "fr": "Git Push", "tr": "Git Push", "de": "Git Push", - "uk": "Git Push" + "uk": "Git Push", + "ca": "Git Push" }, "COMMON$GIT_TOOLS": { "en": "Git Tools", @@ -15213,7 +16163,8 @@ "fr": "Outils Git", "tr": "Git Araçları", "de": "Git-Tools", - "uk": "Інструменти Git" + "uk": "Інструменти Git", + "ca": "Eines de Git" }, "COMMON$GIT_TOOLS_DISABLED_CONTENT": { "en": "Git tools are only available for connected repositories", @@ -15229,7 +16180,8 @@ "ar": "أدوات Git متاحة فقط للمستودعات المتصلة", "no": "Git-verktøy er kun tilgjengelig for tilkoblede depoter", "tr": "Git araçları yalnızca bağlı depolar için kullanılabilir", - "uk": "Інструменти Git доступні лише для підключених репозиторіїв" + "uk": "Інструменти Git доступні лише для підключених репозиторіїв", + "ca": "Les eines de Git només estan disponibles per als repositoris connectats" }, "COMMON$JUPYTER": { "en": "Jupyter", @@ -15245,7 +16197,8 @@ "fr": "Jupyter", "tr": "Jupyter", "de": "Jupyter", - "uk": "Jupyter" + "uk": "Jupyter", + "ca": "Jupyter" }, "COMMON$LANGUAGE_MODEL_LLM": { "en": "Language Model (LLM)", @@ -15261,7 +16214,8 @@ "fr": "Modèle de langue (LLM)", "tr": "Dil Modeli (LLM)", "de": "Sprachmodell (LLM)", - "uk": "Мовна модель (LLM)" + "uk": "Мовна модель (LLM)", + "ca": "Model de llenguatge (LLM)" }, "COMMON$MACROS": { "en": "Macros", @@ -15277,7 +16231,8 @@ "fr": "Macros", "tr": "Makrolar", "de": "Makros", - "uk": "Макроси" + "uk": "Макроси", + "ca": "Macros" }, "COMMON$MODEL_CONTEXT_PROTOCOL_MCP": { "en": "Model Context Protocol (MCP)", @@ -15293,7 +16248,8 @@ "fr": "Protocole de contexte de modèle (MCP)", "tr": "Model Bağlam Protokolü (MCP)", "de": "Modellkontextprotokoll (MCP)", - "uk": "Протокол контексту моделі (MCP)" + "uk": "Протокол контексту моделі (MCP)", + "ca": "Protocol de Context de Model (MCP)" }, "COMMON$NEW_CONVERSATION": { "en": "New Conversation", @@ -15309,7 +16265,8 @@ "fr": "Nouvelle conversation", "tr": "Yeni Konuşma", "de": "Neues Gespräch", - "uk": "Нова розмова" + "uk": "Нова розмова", + "ca": "Nova conversa" }, "COMMON$NO_BRANCH": { "en": "No Branch", @@ -15325,7 +16282,8 @@ "ar": "لا يوجد فرع", "no": "Ingen gren", "tr": "Dal Yok", - "uk": "Гілка відсутня" + "uk": "Гілка відсутня", + "ca": "Sense branca" }, "COMMON$NO_REPOSITORY": { "en": "No Repository", @@ -15341,7 +16299,8 @@ "fr": "Aucun dépôt", "tr": "Depo yok", "de": "Kein Repository", - "uk": "Немає репозиторію" + "uk": "Немає репозиторію", + "ca": "Sense repositori" }, "COMMON$NO_REPO_CONNECTED": { "en": "No Repo Connected", @@ -15357,7 +16316,8 @@ "ar": "لا يوجد مستودع متصل", "no": "Ingen repo tilkoblet", "tr": "Bağlı Depo Yok", - "uk": "Репозиторій не підключено" + "uk": "Репозиторій не підключено", + "ca": "Sense repositori connectat" }, "COMMON$OPEN_REPOSITORY": { "en": "Open Repository", @@ -15373,7 +16333,8 @@ "fr": "Ouvrir le dépôt", "tr": "Depoyu Aç", "de": "Repository öffnen", - "uk": "Відкрити репозиторій" + "uk": "Відкрити репозиторій", + "ca": "Obre el repositori" }, "COMMON$PULL": { "en": "Pull", @@ -15389,7 +16350,8 @@ "ar": "سحب", "no": "Pull", "tr": "Pull", - "uk": "Пул" + "uk": "Пул", + "ca": "Pull" }, "COMMON$PULL_REQUEST": { "en": "Pull Request", @@ -15405,7 +16367,8 @@ "ar": "طلب السحب", "no": "Trekkforespørsel", "tr": "Çekme İsteği", - "uk": "Запит на злиття" + "uk": "Запит на злиття", + "ca": "Sol·licitud de canvis" }, "COMMON$PUSH": { "en": "Push", @@ -15421,7 +16384,8 @@ "ar": "دفع", "no": "Push", "tr": "Push", - "uk": "Пуш" + "uk": "Пуш", + "ca": "Push" }, "COMMON$RECENT_CONVERSATIONS": { "en": "Recent Conversations", @@ -15437,7 +16401,8 @@ "fr": "Conversations récentes", "tr": "Son Konuşmalar", "de": "Kürzliche Gespräche", - "uk": "Останні розмови" + "uk": "Останні розмови", + "ca": "Converses recents" }, "COMMON$RECENT_PROJECTS": { "en": "Recent Projects", @@ -15453,7 +16418,8 @@ "fr": "Projets récents", "tr": "Son Projeler", "de": "Kürzliche Projekte", - "uk": "Останні проєкти" + "uk": "Останні проєкти", + "ca": "Projectes recents" }, "COMMON$RUN": { "en": "Run", @@ -15469,7 +16435,8 @@ "ar": "تشغيل", "no": "Kjør", "tr": "Çalıştır", - "uk": "Запустити" + "uk": "Запустити", + "ca": "Executa" }, "COMMON$RUNNING": { "en": "Running", @@ -15485,7 +16452,8 @@ "fr": "En cours d'exécution", "tr": "Çalışıyor", "de": "Läuft", - "uk": "Працює" + "uk": "Працює", + "ca": "En execució" }, "COMMON$WAITING_FOR_SANDBOX": { "en": "Waiting for sandbox", @@ -15501,7 +16469,8 @@ "fr": "En attente du bac à sable", "tr": "Sandbox bekleniyor", "de": "Warten auf Sandbox", - "uk": "Очікування пісочниці" + "uk": "Очікування пісочниці", + "ca": "Esperant el sandbox" }, "COMMON$SELECT_GIT_PROVIDER": { "en": "Select Git provider", @@ -15517,7 +16486,8 @@ "fr": "Sélectionnez le fournisseur Git", "tr": "Git sağlayıcısı seç", "de": "Git-Anbieter auswählen", - "uk": "Виберіть постачальника Git" + "uk": "Виберіть постачальника Git", + "ca": "Selecciona el proveïdor de Git" }, "COMMON$SERVER_STATUS": { "en": "Server Status", @@ -15533,7 +16503,8 @@ "fr": "Statut du serveur", "tr": "Sunucu Durumu", "de": "Serverstatus", - "uk": "Статус сервера" + "uk": "Статус сервера", + "ca": "Estat del servidor" }, "COMMON$SERVER_STOPPED": { "en": "Server Stopped", @@ -15549,7 +16520,8 @@ "fr": "Serveur arrêté", "tr": "Sunucu durdu", "de": "Server gestoppt", - "uk": "Сервер зупинено" + "uk": "Сервер зупинено", + "ca": "Servidor aturat" }, "COMMON$START_FROM_SCRATCH": { "en": "Start from Scratch", @@ -15565,7 +16537,8 @@ "fr": "Commencer de zéro", "tr": "Sıfırdan Başla", "de": "Von vorne beginnen", - "uk": "Почати з нуля" + "uk": "Почати з нуля", + "ca": "Comença des de zero" }, "COMMON$START_SERVER": { "en": "Start Server", @@ -15581,7 +16554,8 @@ "fr": "Démarrer le serveur", "tr": "Sunucuyu başlat", "de": "Server starten", - "uk": "Запустити сервер" + "uk": "Запустити сервер", + "ca": "Inicia el servidor" }, "COMMON$START_CONVERSATION": { "en": "Start Conversation", @@ -15597,7 +16571,8 @@ "fr": "Démarrer la conversation", "tr": "Sohbeti başlat", "de": "Unterhaltung starten", - "uk": "Почати розмову" + "uk": "Почати розмову", + "ca": "Inicia la conversa" }, "COMMON$STOP_SERVER": { "en": "Stop Server", @@ -15613,7 +16588,8 @@ "fr": "Arrêter le serveur", "tr": "Sunucuyu durdur", "de": "Server stoppen", - "uk": "Зупинити сервер" + "uk": "Зупинити сервер", + "ca": "Atura el servidor" }, "COMMON$TERMINAL": { "en": "Terminal (read-only)", @@ -15629,7 +16605,8 @@ "fr": "Terminal (lecture seule)", "tr": "Terminal (salt okunur)", "de": "Terminal (schreibgeschützt)", - "uk": "Термінал (тільки читання)" + "uk": "Термінал (тільки читання)", + "ca": "Terminal (només lectura)" }, "COMMON$UNKNOWN": { "en": "Unknown", @@ -15645,7 +16622,8 @@ "fr": "Inconnu", "tr": "Bilinmiyor", "de": "Unbekannt", - "uk": "Невідомо" + "uk": "Невідомо", + "ca": "Desconegut" }, "COMMON$USER_SETTINGS": { "en": "User Settings", @@ -15661,7 +16639,8 @@ "fr": "Paramètres utilisateur", "tr": "Kullanıcı Ayarları", "de": "Benutzereinstellungen", - "uk": "Налаштування користувача" + "uk": "Налаштування користувача", + "ca": "Configuració de l'usuari" }, "COMMON$VIEW": { "en": "View", @@ -15677,7 +16656,8 @@ "ar": "عرض", "no": "Vis", "tr": "Görüntüle", - "uk": "Переглянути" + "uk": "Переглянути", + "ca": "Visualitza" }, "COMMON$VIEW_LESS": { "en": "View Less", @@ -15693,7 +16673,8 @@ "fr": "Voir moins", "tr": "Daha Az Göster", "de": "Weniger anzeigen", - "uk": "Переглянути менше" + "uk": "Переглянути менше", + "ca": "Mostra menys" }, "COMMON$VIEW_MORE": { "en": "View More", @@ -15709,7 +16690,8 @@ "fr": "Voir plus", "tr": "Daha Fazla Göster", "de": "Mehr anzeigen", - "uk": "Переглянути більше" + "uk": "Переглянути більше", + "ca": "Mostra més" }, "COMMON$ARCHIVED": { "en": "Archived", @@ -15725,7 +16707,8 @@ "fr": "Archivé", "tr": "Arşivlendi", "de": "Archiviert", - "uk": "Архівовано" + "uk": "Архівовано", + "ca": "Arxivat" }, "HOME$GUIDE_MESSAGE_TITLE": { "en": "New around here? Not sure where to start?", @@ -15741,7 +16724,8 @@ "fr": "Nouveau ici ? Vous ne savez pas par où commencer ?", "tr": "Buralarda yeni misiniz? Nereden başlayacağınızı bilmiyor musunuz?", "de": "Neu hier? Nicht sicher, wo du anfangen sollst?", - "uk": "Вперше тут? Не знаєте, з чого почати?" + "uk": "Вперше тут? Не знаєте, з чого почати?", + "ca": "Sou nou aquí? No sabeu per on començar?" }, "HOME$NEW_PROJECT_DESCRIPTION": { "en": "Start a new conversation that is not connected to an existing repository.", @@ -15757,7 +16741,8 @@ "fr": "Démarrez une nouvelle conversation qui n'est pas connectée à un dépôt existant.", "tr": "Mevcut bir depoya bağlı olmayan yeni bir konuşma başlatın.", "de": "Beginnen Sie ein neues Gespräch, das nicht mit einem bestehenden Repository verbunden ist.", - "uk": "Почніть нову розмову, яка не пов'язана з існуючим репозиторієм." + "uk": "Почніть нову розмову, яка не пов'язана з існуючим репозиторієм.", + "ca": "Inicia una nova conversa que no estigui connectada a un repositori existent." }, "HOME$NO_RECENT_CONVERSATIONS": { "en": "No recent conversations", @@ -15773,7 +16758,8 @@ "fr": "Aucune conversation récente", "tr": "Son konuşma yok", "de": "Keine aktuellen Unterhaltungen", - "uk": "Немає недавніх розмов" + "uk": "Немає недавніх розмов", + "ca": "No hi ha converses recents" }, "HOME$SELECT_OR_INSERT_URL": { "en": "Select or insert a URL", @@ -15789,7 +16775,8 @@ "ar": "اختر أو أدخل عنوان URL", "no": "Velg eller sett inn en URL", "tr": "Bir URL seçin veya girin", - "uk": "Виберіть або вставте URL" + "uk": "Виберіть або вставте URL", + "ca": "Seleccioneu o inseriu una URL" }, "TERMINAL$CONSOLE": { "en": "Console", @@ -15805,7 +16792,8 @@ "fr": "Console", "tr": "Konsol", "de": "Konsole", - "uk": "Консоль" + "uk": "Консоль", + "ca": "Consola" }, "MICROAGENT$DEFINITION": { "en": "Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge. They provide expert guidance, automate common tasks, and ensure consistent practices across projects.", @@ -15821,7 +16809,8 @@ "fr": "Les microagents sont des invites spécialisées qui enrichissent OpenHands avec des connaissances spécifiques au domaine. Ils fournissent des conseils d'experts, automatisent les tâches courantes et garantissent des pratiques cohérentes dans les projets.", "tr": "Mikro ajanlar, OpenHands'i alanına özgü bilgilerle geliştiren özel istemlerdir. Uzman rehberliği sağlar, yaygın görevleri otomatikleştirir ve projeler arasında tutarlı uygulamalar sunar.", "de": "Microagents sind spezialisierte Prompts, die OpenHands mit domänenspezifischem Wissen erweitern. Sie bieten fachkundige Anleitung, automatisieren gängige Aufgaben und sorgen für konsistente Praktiken in Projekten.", - "uk": "Мікроагенти — це спеціалізовані підказки, які розширюють OpenHands галузевими знаннями. Вони надають експертні поради, автоматизують типові завдання та забезпечують послідовні практики у проєктах." + "uk": "Мікроагенти — це спеціалізовані підказки, які розширюють OpenHands галузевими знаннями. Вони надають експертні поради, автоматизують типові завдання та забезпечують послідовні практики у проєктах.", + "ca": "Els microagents són missatges especialitzats que milloren OpenHands amb coneixement específic del domini. Proporcionen orientació d'experts, automatitzen tasques habituals i garanteixen pràctiques coherents en tots els projectes." }, "MICROAGENT$ADD_TO_MEMORY": { "en": "Add to Microagent Memory", @@ -15837,7 +16826,8 @@ "fr": "Ajouter à la mémoire du microagent", "tr": "Mikroajan Hafızasına Ekle", "de": "Zur Microagent-Speicher hinzufügen", - "uk": "Додати до пам'яті мікроагента" + "uk": "Додати до пам'яті мікроагента", + "ca": "Afegeix a la memòria del Microagent" }, "COMMON$IN_PROGRESS": { "en": "In Progress", @@ -15853,7 +16843,8 @@ "fr": "En cours", "tr": "Devam Ediyor", "de": "In Bearbeitung", - "uk": "В процесі" + "uk": "В процесі", + "ca": "En curs" }, "SETTINGS$UPGRADE_BANNER_MESSAGE": { "en": "Access LLM settings when you upgrade your plan", @@ -15869,7 +16860,8 @@ "fr": "Accédez aux paramètres LLM lorsque vous mettez à niveau votre plan", "tr": "Planınızı yükselttiğinizde LLM ayarlarına erişin", "de": "Zugriff auf LLM-Einstellungen beim Upgrade Ihres Plans", - "uk": "Отримайте доступ до налаштувань LLM, коли оновите свій план" + "uk": "Отримайте доступ до налаштувань LLM, коли оновите свій план", + "ca": "Accediu a la configuració del LLM quan actualitzeu el vostre pla" }, "SETTINGS$UPGRADE_BUTTON": { "en": "Upgrade", @@ -15885,7 +16877,8 @@ "fr": "Mettre à niveau", "tr": "Yükselt", "de": "Upgrade", - "uk": "Оновити" + "uk": "Оновити", + "ca": "Actualitza" }, "SETTINGS$PRO_PILL": { "en": "Pro", @@ -15901,7 +16894,8 @@ "fr": "Pro", "tr": "Pro", "de": "Pro", - "uk": "Про" + "uk": "Про", + "ca": "Pro" }, "COMMON$STOP_RUNTIME": { "en": "Stop Runtime", @@ -15917,7 +16911,8 @@ "fr": "Arrêter le runtime", "tr": "Çalışma zamanını durdur", "de": "Runtime stoppen", - "uk": "Зупинити середовище виконання" + "uk": "Зупинити середовище виконання", + "ca": "Atura l'entorn d'execució" }, "COMMON$START_RUNTIME": { "en": "Start Runtime", @@ -15933,7 +16928,8 @@ "fr": "Démarrer le runtime", "tr": "Çalışma zamanını başlat", "de": "Runtime starten", - "uk": "Запустити середовище виконання" + "uk": "Запустити середовище виконання", + "ca": "Inicia l'entorn d'execució" }, "COMMON$JUPYTER_EMPTY_MESSAGE": { "en": "Your Jupyter notebook is empty. No cells to display.", @@ -15949,7 +16945,8 @@ "fr": "Votre notebook Jupyter est vide. Aucune cellule à afficher.", "tr": "Jupyter defteriniz boş. Gösterilecek hücre yok.", "de": "Ihr Jupyter-Notebook ist leer. Keine Zellen zum Anzeigen.", - "uk": "Ваш Jupyter-ноутбук порожній. Немає клітинок для відображення." + "uk": "Ваш Jupyter-ноутбук порожній. Немає клітинок для відображення.", + "ca": "El vostre bloc de notes Jupyter és buit. No hi ha cel·les per mostrar." }, "COMMON$CONFIRMATION_MODE_ENABLED": { "en": "Confirmation mode enabled", @@ -15965,7 +16962,8 @@ "fr": "Mode de confirmation activé", "tr": "Onay modu etkinleştirildi", "de": "Bestätigungsmodus aktiviert", - "uk": "Режим підтвердження увімкнено" + "uk": "Режим підтвердження увімкнено", + "ca": "Mode de confirmació activat" }, "COMMON$MOST_RECENT": { "en": "Most Recent", @@ -15981,7 +16979,8 @@ "fr": "Le plus récent", "tr": "En Son", "de": "Neueste", - "uk": "Найновіше" + "uk": "Найновіше", + "ca": "Més recent" }, "HOME$NO_REPOSITORY_FOUND": { "en": "No repository found to launch conversation", @@ -15997,7 +16996,8 @@ "fr": "Aucun dépôt trouvé pour lancer la conversation", "tr": "Konuşma başlatmak için depo bulunamadı", "de": "Kein Repository gefunden, um das Gespräch zu starten", - "uk": "Не знайдено репозиторій для запуску розмови" + "uk": "Не знайдено репозиторій для запуску розмови", + "ca": "No s'ha trobat cap repositori per llançar la conversa" }, "CONVERSATION$VERSION_V1_NEW": { "en": "Conversation API Version 1 (New)", @@ -16013,7 +17013,8 @@ "fr": "API de conversation version 1 (Nouvelle)", "tr": "Konuşma API'si Sürüm 1 (Yeni)", "de": "Konversations-API Version 1 (Neu)", - "uk": "API розмови версія 1 (Нова)" + "uk": "API розмови версія 1 (Нова)", + "ca": "Versió 1 de l'API de conversa (nova)" }, "CONVERSATION$VERSION_V0_LEGACY": { "en": "Conversation API Version 0 (Legacy)", @@ -16029,7 +17030,8 @@ "fr": "API de conversation version 0 (Ancienne)", "tr": "Konuşma API'si Sürüm 0 (Eski)", "de": "Konversations-API Version 0 (Legacy)", - "uk": "API розмови версія 0 (Застаріла)" + "uk": "API розмови версія 0 (Застаріла)", + "ca": "Versió 0 de l'API de conversa (llegat)" }, "CONVERSATION$ERROR_STARTING_CONVERSATION": { "en": "Error starting conversation", @@ -16045,7 +17047,8 @@ "fr": "Erreur lors du démarrage de la conversation", "tr": "Konuşma başlatılırken hata", "de": "Fehler beim Starten der Konversation", - "uk": "Помилка запуску розмови" + "uk": "Помилка запуску розмови", + "ca": "Error en iniciar la conversa" }, "CONVERSATION$READY": { "en": "Ready", @@ -16061,7 +17064,8 @@ "fr": "Prêt", "tr": "Hazır", "de": "Bereit", - "uk": "Готово" + "uk": "Готово", + "ca": "Preparat" }, "CONVERSATION$STARTING_CONVERSATION": { "en": "Starting conversation...", @@ -16077,7 +17081,8 @@ "fr": "Démarrage de la conversation...", "tr": "Konuşma başlatılıyor...", "de": "Konversation wird gestartet...", - "uk": "Запуск розмови..." + "uk": "Запуск розмови...", + "ca": "Iniciant la conversa..." }, "CONVERSATION$FAILED_TO_START_FROM_TASK": { "en": "Failed to start the conversation from task.", @@ -16093,7 +17098,8 @@ "fr": "Échec du démarrage de la conversation depuis la tâche.", "tr": "Görevden konuşma başlatılamadı.", "de": "Konversation konnte nicht aus Aufgabe gestartet werden.", - "uk": "Не вдалося запустити розмову із завдання." + "uk": "Не вдалося запустити розмову із завдання.", + "ca": "No s'ha pogut iniciar la conversa des de la tasca." }, "CONVERSATION$NOT_EXIST_OR_NO_PERMISSION": { "en": "This conversation does not exist, or you do not have permission to access it. If this is your conversation, try switching to the workspace where it was created.", @@ -16109,7 +17115,8 @@ "fr": "Cette conversation n'existe pas ou vous n'avez pas la permission d'y accéder. S'il s'agit de votre conversation, essayez de passer à l'espace de travail où elle a été créée.", "tr": "Bu konuşma mevcut değil veya erişim izniniz yok. Bu sizin konuşmanızsa, oluşturulduğu çalışma alanına geçmeyi deneyin.", "de": "Diese Konversation existiert nicht oder Sie haben keine Berechtigung darauf zuzugreifen. Wenn dies Ihre Konversation ist, versuchen Sie zum Arbeitsbereich zu wechseln, in dem sie erstellt wurde.", - "uk": "Ця розмова не існує або у вас немає дозволу на доступ до неї. Якщо це ваша розмова, спробуйте перейти до робочої області, де вона була створена." + "uk": "Ця розмова не існує або у вас немає дозволу на доступ до неї. Якщо це ваша розмова, спробуйте перейти до робочої області, де вона була створена.", + "ca": "Aquesta conversa no existeix o no teniu permís per accedir-hi." }, "CONVERSATION$FAILED_TO_START_WITH_ERROR": { "en": "Failed to start conversation: {{error}}", @@ -16125,7 +17132,8 @@ "fr": "Échec du démarrage de la conversation : {{error}}", "tr": "Konuşma başlatılamadı: {{error}}", "de": "Konversation konnte nicht gestartet werden: {{error}}", - "uk": "Не вдалося запустити розмову: {{error}}" + "uk": "Не вдалося запустити розмову: {{error}}", + "ca": "No s'ha pogut iniciar la conversa: {{error}}" }, "TOAST$STOPPING_CONVERSATION": { "en": "Stopping conversation...", @@ -16141,7 +17149,8 @@ "fr": "Arrêt de la conversation...", "tr": "Konuşma durduruluyor...", "de": "Konversation wird gestoppt...", - "uk": "Зупинка розмови..." + "uk": "Зупинка розмови...", + "ca": "Aturant la conversa..." }, "TOAST$FAILED_TO_STOP_CONVERSATION": { "en": "Failed to stop conversation", @@ -16157,7 +17166,8 @@ "fr": "Échec de l'arrêt de la conversation", "tr": "Konuşma durdurulamadı", "de": "Konversation konnte nicht gestoppt werden", - "uk": "Не вдалося зупинити розмову" + "uk": "Не вдалося зупинити розмову", + "ca": "No s'ha pogut aturar la conversa" }, "TOAST$CONVERSATION_STOPPED": { "en": "Conversation stopped", @@ -16173,7 +17183,8 @@ "fr": "Conversation arrêtée", "tr": "Konuşma durduruldu", "de": "Konversation gestoppt", - "uk": "Розмову зупинено" + "uk": "Розмову зупинено", + "ca": "Conversa aturada" }, "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION": { "en": "Waiting for user confirmation", @@ -16189,7 +17200,8 @@ "fr": "En attente de la confirmation de l'utilisateur", "tr": "Kullanıcı onayı bekleniyor", "de": "Warte auf Benutzerbestätigung", - "uk": "Очікується підтвердження користувача" + "uk": "Очікується підтвердження користувача", + "ca": "Esperant la confirmació de l'usuari" }, "COMMON$MORE_OPTIONS": { "en": "More options", @@ -16205,7 +17217,8 @@ "fr": "Plus d'options", "tr": "Daha fazla seçenek", "de": "Weitere Optionen", - "uk": "Більше опцій" + "uk": "Більше опцій", + "ca": "Més opcions" }, "COMMON$CREATE_A_PLAN": { "en": "Create a plan", @@ -16221,7 +17234,8 @@ "fr": "Créer un plan", "tr": "Bir plan oluştur", "de": "Einen Plan erstellen", - "uk": "Створити план" + "uk": "Створити план", + "ca": "Crea un pla" }, "COMMON$TASKS": { "en": "Tasks", @@ -16237,7 +17251,8 @@ "fr": "Tâches", "tr": "Görevler", "de": "Aufgaben", - "uk": "Завдання" + "uk": "Завдання", + "ca": "Tasques" }, "COMMON$TASK_LIST": { "en": "Task List", @@ -16253,7 +17268,8 @@ "fr": "Liste des tâches", "tr": "Görev listesi", "de": "Aufgabenliste", - "uk": "Список завдань" + "uk": "Список завдань", + "ca": "Llista de tasques" }, "COMMON$NO_TASKS": { "en": "No tasks yet", @@ -16269,7 +17285,8 @@ "fr": "Aucune tâche pour le moment", "tr": "Henüz görev yok", "de": "Noch keine Aufgaben", - "uk": "Завдань поки немає" + "uk": "Завдань поки немає", + "ca": "Encara no hi ha tasques" }, "COMMON$PLAN_MD": { "en": "Plan.md", @@ -16285,7 +17302,8 @@ "fr": "Plan.md", "tr": "Plan.md", "de": "Plan.md", - "uk": "Plan.md" + "uk": "Plan.md", + "ca": "Pla.md" }, "COMMON$READ_MORE": { "en": "Read more", @@ -16301,7 +17319,8 @@ "fr": "En savoir plus", "tr": "Devamını oku", "de": "Mehr lesen", - "uk": "Читати далі" + "uk": "Читати далі", + "ca": "Llegeix més" }, "COMMON$BUILD": { "en": "Build", @@ -16317,7 +17336,8 @@ "fr": "Construire", "tr": "Derle", "de": "Erstellen", - "uk": "Зібрати" + "uk": "Зібрати", + "ca": "Construeix" }, "COMMON$ASK": { "en": "Ask", @@ -16333,7 +17353,8 @@ "fr": "Demander", "tr": "Sor", "de": "Fragen", - "uk": "Запитати" + "uk": "Запитати", + "ca": "Pregunta" }, "COMMON$PLAN": { "en": "Plan", @@ -16349,7 +17370,8 @@ "fr": "Planifier", "tr": "Plan", "de": "Plan", - "uk": "План" + "uk": "План", + "ca": "Pla" }, "COMMON$LET_S_WORK_ON_A_PLAN": { "en": "Let’s work on a plan", @@ -16365,7 +17387,8 @@ "fr": "Travaillons sur un plan", "tr": "Bir plan üzerinde çalışalım", "de": "Lassen Sie uns an einem Plan arbeiten", - "uk": "Давайте розробимо план" + "uk": "Давайте розробимо план", + "ca": "Treballem en un pla" }, "COMMON$CODE_AGENT_DESCRIPTION": { "en": "Write, edit, and debug with AI assistance in real time.", @@ -16381,7 +17404,8 @@ "fr": "Rédigez, modifiez et déboguez avec l’aide de l’IA en temps réel.", "tr": "AI desteğiyle gerçek zamanlı olarak yazın, düzenleyin ve hata ayıklayın.", "de": "Schreiben, bearbeiten und debuggen Sie mit KI-Unterstützung in Echtzeit.", - "uk": "Пишіть, редагуйте та налагоджуйте з підтримкою ШІ у реальному часі." + "uk": "Пишіть, редагуйте та налагоджуйте з підтримкою ШІ у реальному часі.", + "ca": "Escriu, edita i depura amb assistència d'IA en temps real." }, "COMMON$PLAN_AGENT_DESCRIPTION": { "en": "Outline goals, structure tasks, and map your next steps.", @@ -16397,7 +17421,8 @@ "fr": "Dressez des objectifs, structurez vos tâches et planifiez vos prochaines étapes.", "tr": "Hedefleri belirtin, görevleri yapılandırın ve sonraki adımlarınızı belirleyin.", "de": "Umreißen Sie Ziele, strukturieren Sie Aufgaben und planen Sie Ihre nächsten Schritte.", - "uk": "Окресліть цілі, структуруйте завдання та сплануйте наступні кроки." + "uk": "Окресліть цілі, структуруйте завдання та сплануйте наступні кроки.", + "ca": "Defineix objectius, estructura tasques i planifica els propers passos." }, "PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED": { "en": "Planning agent initialized", @@ -16413,7 +17438,8 @@ "fr": "Agent de planification initialisé", "tr": "Planlama ajanı başlatıldı", "de": "Planungsagent wurde initialisiert", - "uk": "Агент планування ініціалізовано" + "uk": "Агент планування ініціалізовано", + "ca": "Agent de planificació inicialitzat" }, "OBSERVATION_MESSAGE$SKILL_READY": { "en": "Skill Ready", @@ -16429,7 +17455,8 @@ "fr": "Compétence prête", "tr": "Yetenek hazır", "de": "Fähigkeit bereit", - "uk": "Навичка готова" + "uk": "Навичка готова", + "ca": "Habilitat preparada" }, "ORG$ORGANIZATION_NAME": { "en": "Organization Name", @@ -16446,6 +17473,8 @@ "tr": "Organizasyon Adı", "de": "Organisationsname", "uk": "Назва організації" + , + "ca": "Nom de l'organització" }, "ORG$NEXT": { "en": "Next", @@ -16461,7 +17490,8 @@ "fr": "Suivant", "tr": "İleri", "de": "Weiter", - "uk": "Далі" + "uk": "Далі", + "ca": "Següent" }, "ORG$INVITE_USERS_DESCRIPTION": { "en": "Invite colleagues using their email address", @@ -16477,7 +17507,8 @@ "fr": "Invitez des collègues en utilisant leur adresse email", "tr": "E-posta adresi kullanarak meslektaşlarını davet et", "de": "Laden Sie Kollegen per E-Mail-Adresse ein", - "uk": "Запросіть колег за їхньою електронною адресою" + "uk": "Запросіть колег за їхньою електронною адресою", + "ca": "Convideu col·legues mitjançant la seva adreça de correu electrònic" }, "ORG$EMAILS": { "en": "Emails", @@ -16493,7 +17524,8 @@ "fr": "E-mails", "tr": "E-postalar", "de": "E-Mails", - "uk": "Електронні листи" + "uk": "Електронні листи", + "ca": "Correus electrònics" }, "ORG$STATUS_INVITED": { "en": "invited", @@ -16509,7 +17541,8 @@ "fr": "invité", "tr": "davet edildi", "de": "eingeladen", - "uk": "запрошений" + "uk": "запрошений", + "ca": "convidat" }, "ORG$ROLE_ADMIN": { "en": "admin", @@ -16525,7 +17558,8 @@ "fr": "admin", "tr": "yönetici", "de": "Admin", - "uk": "адміністратор" + "uk": "адміністратор", + "ca": "administrador" }, "ORG$ROLE_MEMBER": { "en": "member", @@ -16541,7 +17575,8 @@ "fr": "membre", "tr": "üye", "de": "Mitglied", - "uk": "учасник" + "uk": "учасник", + "ca": "membre" }, "ORG$ROLE_OWNER": { "en": "owner", @@ -16557,7 +17592,8 @@ "fr": "propriétaire", "tr": "sahip", "de": "Eigentümer", - "uk": "власник" + "uk": "власник", + "ca": "propietari" }, "ORG$REMOVE": { "en": "remove", @@ -16573,7 +17609,8 @@ "fr": "supprimer", "tr": "kaldır", "de": "entfernen", - "uk": "видалити" + "uk": "видалити", + "ca": "elimina" }, "ORG$CONFIRM_REMOVE_MEMBER": { "en": "Confirm Remove Member", @@ -16589,7 +17626,8 @@ "fr": "Confirmer la suppression du membre", "tr": "Üye kaldırma onayı", "de": "Mitglied entfernen bestätigen", - "uk": "Підтвердити видалення учасника" + "uk": "Підтвердити видалення учасника", + "ca": "Confirma l'eliminació del membre" }, "ORG$REMOVE_MEMBER_WARNING": { "en": "Are you sure you want to remove {{email}} from this organization? This action cannot be undone.", @@ -16605,7 +17643,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer {{email}} de cette organisation ? Cette action ne peut pas être annulée.", "tr": "{{email}} kullanıcısını bu organizasyondan kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz.", "de": "Sind Sie sicher, dass Sie {{email}} aus dieser Organisation entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "uk": "Ви впевнені, що хочете видалити {{email}} з цієї організації? Цю дію неможливо скасувати." + "uk": "Ви впевнені, що хочете видалити {{email}} з цієї організації? Цю дію неможливо скасувати.", + "ca": "Esteu segur que voleu eliminar {{email}} d'aquesta organització? Aquesta acció no es pot desfer." }, "ORG$REMOVE_MEMBER_ERROR": { "en": "Failed to remove member from organization. Please try again.", @@ -16621,7 +17660,8 @@ "fr": "Échec de la suppression du membre de l'organisation. Veuillez réessayer.", "tr": "Üye organizasyondan kaldırılamadı. Lütfen tekrar deneyin.", "de": "Mitglied konnte nicht aus der Organisation entfernt werden. Bitte versuchen Sie es erneut.", - "uk": "Не вдалося видалити учасника з організації. Будь ласка, спробуйте ще раз." + "uk": "Не вдалося видалити учасника з організації. Будь ласка, спробуйте ще раз.", + "ca": "No s'ha pogut eliminar el membre de l'organització. Torneu-ho a intentar." }, "ORG$REMOVE_MEMBER_SUCCESS": { "en": "Member removed successfully", @@ -16637,7 +17677,8 @@ "fr": "Membre supprimé avec succès", "tr": "Üye başarıyla kaldırıldı", "de": "Mitglied erfolgreich entfernt", - "uk": "Учасника успішно видалено" + "uk": "Учасника успішно видалено", + "ca": "Membre eliminat correctament" }, "ORG$CONFIRM_UPDATE_ROLE": { "en": "Confirm Role Update", @@ -16653,7 +17694,8 @@ "fr": "Confirmer la mise à jour du rôle", "tr": "Rol güncellemesini onayla", "de": "Rollenaktualisierung bestätigen", - "uk": "Підтвердити оновлення ролі" + "uk": "Підтвердити оновлення ролі", + "ca": "Confirma l'actualització del rol" }, "ORG$UPDATE_ROLE_WARNING": { "en": "Are you sure you want to change the role of {{email}} to {{role}}?", @@ -16669,7 +17711,8 @@ "fr": "Êtes-vous sûr de vouloir changer le rôle de {{email}} en {{role}} ?", "tr": "{{email}} kullanıcısının rolünü {{role}} olarak değiştirmek istediğinizden emin misiniz?", "de": "Sind Sie sicher, dass Sie die Rolle von {{email}} auf {{role}} ändern möchten?", - "uk": "Ви впевнені, що хочете змінити роль {{email}} на {{role}}?" + "uk": "Ви впевнені, що хочете змінити роль {{email}} на {{role}}?", + "ca": "Esteu segur que voleu canviar el rol de {{email}} a {{role}}?" }, "ORG$UPDATE_ROLE_SUCCESS": { "en": "Role updated successfully", @@ -16685,7 +17728,8 @@ "fr": "Rôle mis à jour avec succès", "tr": "Rol başarıyla güncellendi", "de": "Rolle erfolgreich aktualisiert", - "uk": "Роль успішно оновлено" + "uk": "Роль успішно оновлено", + "ca": "Rol actualitzat correctament" }, "ORG$UPDATE_ROLE_ERROR": { "en": "Failed to update role. Please try again.", @@ -16701,7 +17745,8 @@ "fr": "Échec de la mise à jour du rôle. Veuillez réessayer.", "tr": "Rol güncellenemedi. Lütfen tekrar deneyin.", "de": "Rolle konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.", - "uk": "Не вдалося оновити роль. Будь ласка, спробуйте ще раз." + "uk": "Не вдалося оновити роль. Будь ласка, спробуйте ще раз.", + "ca": "No s'ha pogut actualitzar el rol. Torneu-ho a intentar." }, "ORG$INVITE_MEMBERS_SUCCESS": { "en": "Invitations sent successfully", @@ -16717,7 +17762,8 @@ "fr": "Invitations envoyées avec succès", "tr": "Davetler başarıyla gönderildi", "de": "Einladungen erfolgreich gesendet", - "uk": "Запрошення успішно надіслано" + "uk": "Запрошення успішно надіслано", + "ca": "Invitacions enviades correctament" }, "ORG$INVITE_MEMBERS_ERROR": { "en": "Failed to send invitations. Please try again.", @@ -16733,7 +17779,8 @@ "fr": "Échec de l'envoi des invitations. Veuillez réessayer.", "tr": "Davetler gönderilemedi. Lütfen tekrar deneyin.", "de": "Einladungen konnten nicht gesendet werden. Bitte versuchen Sie es erneut.", - "uk": "Не вдалося надіслати запрошення. Будь ласка, спробуйте ще раз." + "uk": "Не вдалося надіслати запрошення. Будь ласка, спробуйте ще раз.", + "ca": "No s'han pogut enviar les invitacions. Torneu-ho a intentar." }, "ORG$DUPLICATE_EMAILS_ERROR": { "en": "Duplicate email addresses are not allowed", @@ -16749,7 +17796,8 @@ "fr": "Les adresses e-mail en double ne sont pas autorisées", "tr": "Yinelenen e-posta adreslerine izin verilmiyor", "de": "Doppelte E-Mail-Adressen sind nicht erlaubt", - "uk": "Дублікати електронних адрес не допускаються" + "uk": "Дублікати електронних адрес не допускаються", + "ca": "No s'admeten adreces de correu electrònic duplicades" }, "ORG$NO_EMAILS_ADDED_HINT": { "en": "Please type emails and then press space.", @@ -16765,7 +17813,8 @@ "fr": "Veuillez saisir les e-mails puis appuyer sur espace.", "tr": "Lütfen e-postaları yazın ve ardından boşluk tuşuna basın.", "de": "Bitte geben Sie E-Mails ein und drücken Sie dann die Leertaste.", - "uk": "Будь ласка, введіть електронні адреси та натисніть пробіл." + "uk": "Будь ласка, введіть електронні адреси та натисніть пробіл.", + "ca": "Escriviu els correus electrònics i premeu espai." }, "ORG$ACCOUNT": { "en": "Account", @@ -16781,7 +17830,8 @@ "fr": "Compte", "tr": "Hesap", "de": "Konto", - "uk": "Обліковий запис" + "uk": "Обліковий запис", + "ca": "Compte" }, "ORG$INVITE_TEAM": { "en": "Invite Team", @@ -16797,7 +17847,8 @@ "fr": "Inviter l'équipe", "tr": "Takım Davet Et", "de": "Team einladen", - "uk": "Запросити команду" + "uk": "Запросити команду", + "ca": "Convida l'equip" }, "ORG$MANAGE_TEAM": { "en": "Manage Team", @@ -16813,7 +17864,8 @@ "fr": "Gérer l'équipe", "tr": "Takımı Yönet", "de": "Team verwalten", - "uk": "Керувати командою" + "uk": "Керувати командою", + "ca": "Gestiona l'equip" }, "ORG$CHANGE_ORG_NAME": { "en": "Change Organization Name", @@ -16829,7 +17881,8 @@ "fr": "Changer le nom de l'organisation", "tr": "Organizasyon Adını Değiştir", "de": "Organisationsnamen ändern", - "uk": "Змінити назву організації" + "uk": "Змінити назву організації", + "ca": "Canvia el nom de l'organització" }, "ORG$MODIFY_ORG_NAME_DESCRIPTION": { "en": "Modify your Organization Name and Save", @@ -16845,7 +17898,8 @@ "fr": "Modifiez le nom de votre organisation et enregistrez", "tr": "Organizasyon adınızı değiştirin ve kaydedin", "de": "Ändern Sie den Namen Ihrer Organisation und speichern Sie ihn", - "uk": "Змініть назву вашої організації та збережіть" + "uk": "Змініть назву вашої організації та збережіть", + "ca": "Modifiqueu el nom de la vostra organització i deseu" }, "ORG$ADD_CREDITS": { "en": "Add Credits", @@ -16861,7 +17915,8 @@ "fr": "Ajouter des crédits", "tr": "Kredi Ekle", "de": "Credits hinzufügen", - "uk": "Додати кредити" + "uk": "Додати кредити", + "ca": "Afegeix crèdits" }, "ORG$CREDITS": { "en": "Credits", @@ -16877,7 +17932,8 @@ "fr": "Crédits", "tr": "Krediler", "de": "Credits", - "uk": "Кредити" + "uk": "Кредити", + "ca": "Crèdits" }, "ORG$ADD": { "en": "+ Add", @@ -16893,7 +17949,8 @@ "fr": "+ Ajouter", "tr": "+ Ekle", "de": "+ Hinzufügen", - "uk": "+ Додати" + "uk": "+ Додати", + "ca": "+ Afegeix" }, "ORG$BILLING_INFORMATION": { "en": "Billing Information", @@ -16909,7 +17966,8 @@ "fr": "Informations de facturation", "tr": "Fatura Bilgisi", "de": "Rechnungsinformationen", - "uk": "Платіжна інформація" + "uk": "Платіжна інформація", + "ca": "Informació de facturació" }, "ORG$CHANGE": { "en": "Change", @@ -16925,7 +17983,8 @@ "fr": "Modifier", "tr": "Değiştir", "de": "Ändern", - "uk": "Змінити" + "uk": "Змінити", + "ca": "Canvia" }, "ORG$DELETE_ORGANIZATION": { "en": "Delete Organization", @@ -16941,7 +18000,8 @@ "fr": "Supprimer l'organisation", "tr": "Organizasyonu Sil", "de": "Organisation löschen", - "uk": "Видалити організацію" + "uk": "Видалити організацію", + "ca": "Elimina l'organització" }, "ORG$DELETE_ORGANIZATION_WARNING": { "en": "Are you sure you want to delete this organization? This action cannot be undone.", @@ -16957,7 +18017,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer cette organisation ? Cette action ne peut pas être annulée.", "tr": "Bu organizasyonu silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "de": "Sind Sie sicher, dass Sie diese Organisation löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "uk": "Ви впевнені, що хочете видалити цю організацію? Цю дію не можна скасувати." + "uk": "Ви впевнені, що хочете видалити цю організацію? Цю дію не можна скасувати.", + "ca": "Esteu segur que voleu eliminar aquesta organització? Aquesta acció no es pot desfer." }, "ORG$DELETE_ORGANIZATION_WARNING_WITH_NAME": { "en": "Are you sure you want to delete the \"{{name}}\" organization? This action cannot be undone.", @@ -16973,7 +18034,8 @@ "fr": "Êtes-vous sûr de vouloir supprimer l'organisation \\u00AB {{name}} \\u00BB ? Cette action ne peut pas être annulée.", "tr": "\"{{name}}\" organizasyonunu silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "de": "Sind Sie sicher, dass Sie die Organisation \\u201E{{name}}\\u201C löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", - "uk": "Ви впевнені, що хочете видалити організацію \\u00AB{{name}}\\u00BB? Цю дію не можна скасувати." + "uk": "Ви впевнені, що хочете видалити організацію \\u00AB{{name}}\\u00BB? Цю дію не можна скасувати.", + "ca": "Esteu segur que voleu eliminar l'organització \"{{name}}\"? Aquesta acció no es pot desfer." }, "ORG$DELETE_ORGANIZATION_ERROR": { "en": "Failed to delete organization", @@ -16989,7 +18051,8 @@ "fr": "Échec de la suppression de l'organisation", "tr": "Organizasyon silinemedi", "de": "Organisation konnte nicht gelöscht werden", - "uk": "Не вдалося видалити організацію" + "uk": "Не вдалося видалити організацію", + "ca": "No s'ha pogut eliminar l'organització" }, "ACCOUNT_SETTINGS$SETTINGS": { "en": "Settings", @@ -17005,7 +18068,8 @@ "fr": "Paramètres", "tr": "Ayarlar", "de": "Einstellungen", - "uk": "Налаштування" + "uk": "Налаштування", + "ca": "Configuració" }, "ORG$MANAGE_ORGANIZATION_MEMBERS": { "en": "Manage Organization Members", @@ -17021,7 +18085,8 @@ "fr": "Gérer les membres de l'organisation", "tr": "Organizasyon Üyelerini Yönet", "de": "Organisationsmitglieder verwalten", - "uk": "Керувати учасниками організації" + "uk": "Керувати учасниками організації", + "ca": "Gestiona els membres de l'organització" }, "ORG$SELECT_ORGANIZATION_PLACEHOLDER": { "en": "Please select an organization", @@ -17037,7 +18102,8 @@ "fr": "Veuillez sélectionner une organisation", "tr": "Lütfen bir organizasyon seçin", "de": "Bitte wählen Sie eine Organisation", - "uk": "Будь ласка, виберіть організацію" + "uk": "Будь ласка, виберіть організацію", + "ca": "Seleccioneu una organització" }, "ORG$PERSONAL_WORKSPACE": { "en": "Personal Workspace", @@ -17053,7 +18119,8 @@ "fr": "Espace de travail personnel", "tr": "Kişisel çalışma alanı", "de": "Persönlicher Arbeitsbereich", - "uk": "Особистий робочий простір" + "uk": "Особистий робочий простір", + "ca": "Espai de treball personal" }, "ORG$ENTER_NEW_ORGANIZATION_NAME": { "en": "Enter new organization name", @@ -17069,7 +18136,8 @@ "fr": "Entrez le nouveau nom de l'organisation", "tr": "Yeni organizasyon adını girin", "de": "Geben Sie den neuen Organisationsnamen ein", - "uk": "Введіть нову назву організації" + "uk": "Введіть нову назву організації", + "ca": "Introduïu el nou nom de l'organització" }, "CONVERSATION$SHOW_SKILLS": { "en": "Show Available Skills", @@ -17085,7 +18153,8 @@ "pt": "Mostrar habilidades disponíveis", "es": "Mostrar habilidades disponibles", "tr": "Kullanılabilir yetenekleri göster", - "uk": "Показати доступні навички" + "uk": "Показати доступні навички", + "ca": "Mostra les habilitats disponibles" }, "SKILLS_MODAL$TITLE": { "en": "Available Skills", @@ -17101,7 +18170,8 @@ "pt": "Habilidades disponíveis", "es": "Habilidades disponibles", "tr": "Kullanılabilir yetenekler", - "uk": "Доступні навички" + "uk": "Доступні навички", + "ca": "Habilitats disponibles" }, "CONVERSATION$SHARE_PUBLICLY": { "en": "Public Share", @@ -17117,7 +18187,8 @@ "fr": "Partager publiquement", "tr": "Herkese açık paylaş", "de": "Öffentlich teilen", - "uk": "Поділитися публічно" + "uk": "Поділитися публічно", + "ca": "Comparteix públicament" }, "CONVERSATION$PUBLIC_SHARING_UPDATED": { "en": "Public sharing updated", @@ -17133,7 +18204,8 @@ "fr": "Partage public mis à jour", "tr": "Herkese açık paylaşım güncellendi", "de": "Öffentliche Freigabe aktualisiert", - "uk": "Публічний доступ оновлено" + "uk": "Публічний доступ оновлено", + "ca": "Compartició pública actualitzada" }, "CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING": { "en": "Failed to update public sharing", @@ -17149,7 +18221,8 @@ "fr": "Échec de la mise à jour du partage public", "tr": "Herkese açık paylaşım güncellenemedi", "de": "Fehler beim Aktualisieren der öffentlichen Freigabe", - "uk": "Не вдалося оновити публічний доступ" + "uk": "Не вдалося оновити публічний доступ", + "ca": "No s'ha pogut actualitzar la compartició pública" }, "CONVERSATION$REPOSITORY_UPDATED": { "en": "Repository updated successfully", @@ -17165,7 +18238,8 @@ "fr": "Dépôt mis à jour avec succès", "tr": "Depo başarıyla güncellendi", "de": "Repository erfolgreich aktualisiert", - "uk": "Репозиторій успішно оновлено" + "uk": "Репозиторій успішно оновлено", + "ca": "Repositori actualitzat correctament" }, "CONVERSATION$FAILED_TO_UPDATE_REPOSITORY": { "en": "Failed to update repository", @@ -17181,7 +18255,8 @@ "fr": "Échec de la mise à jour du dépôt", "tr": "Depo güncellenemedi", "de": "Fehler beim Aktualisieren des Repositorys", - "uk": "Не вдалося оновити репозиторій" + "uk": "Не вдалося оновити репозиторій", + "ca": "No s'ha pogut actualitzar el repositori" }, "CONVERSATION$CLONE_COMMAND_FAILED_DISCONNECTED": { "en": "Repository updated but clone command could not be sent. Please reconnect and manually clone.", @@ -17197,7 +18272,8 @@ "fr": "Dépôt mis à jour mais la commande de clonage n'a pas pu être envoyée. Reconnectez-vous et clonez manuellement.", "tr": "Depo güncellendi ancak klonlama komutu gönderilemedi. Lütfen yeniden bağlanın ve manuel olarak klonlayın.", "de": "Repository aktualisiert, aber Klon-Befehl konnte nicht gesendet werden. Bitte erneut verbinden und manuell klonen.", - "uk": "Репозиторій оновлено, але команду клонування не вдалося надіслати. Підключіться знову та клонуйте вручну." + "uk": "Репозиторій оновлено, але команду клонування не вдалося надіслати. Підключіться знову та клонуйте вручну.", + "ca": "Repositori actualitzat però no s'ha pogut enviar la comanda de clonació. Reconnecteu i cloneu manualment." }, "CONVERSATION$CHANGE_REPOSITORY": { "en": "Change Repository", @@ -17213,7 +18289,8 @@ "fr": "Changer de dépôt", "tr": "Depoyu değiştir", "de": "Repository ändern", - "uk": "Змінити репозиторій" + "uk": "Змінити репозиторій", + "ca": "Canvia el repositori" }, "CONVERSATION$ATTACH_REPOSITORY": { "en": "Attach Repository", @@ -17229,7 +18306,8 @@ "fr": "Attacher un dépôt", "tr": "Depo ekle", "de": "Repository anhängen", - "uk": "Прикріпити репозиторій" + "uk": "Прикріпити репозиторій", + "ca": "Adjunta el repositori" }, "CONVERSATION$OPEN_REPOSITORY": { "en": "Open Repository", @@ -17245,7 +18323,8 @@ "fr": "Ouvrir le dépôt", "tr": "Depoyu aç", "de": "Repository öffnen", - "uk": "Відкрити репозиторій" + "uk": "Відкрити репозиторій", + "ca": "Obre el repositori" }, "CONVERSATION$SELECT_OR_INSERT_LINK": { "en": "Select or insert a link", @@ -17261,7 +18340,8 @@ "fr": "Sélectionner ou insérer un lien", "tr": "Bir bağlantı seçin veya ekleyin", "de": "Link auswählen oder einfügen", - "uk": "Виберіть або вставте посилання" + "uk": "Виберіть або вставте посилання", + "ca": "Seleccioneu o inseriu un enllaç" }, "CONVERSATION$NO_REPO_CONNECTED": { "en": "No Repo Connected", @@ -17277,7 +18357,8 @@ "fr": "Aucun dépôt connecté", "tr": "Bağlı depo yok", "de": "Kein Repository verbunden", - "uk": "Репозиторій не підключено" + "uk": "Репозиторій не підключено", + "ca": "Sense repositori connectat" }, "CONVERSATION$NOT_FOUND": { "en": "Conversation not found", @@ -17293,7 +18374,8 @@ "fr": "Conversation introuvable", "tr": "Konuşma bulunamadı", "de": "Unterhaltung nicht gefunden", - "uk": "Розмову не знайдено" + "uk": "Розмову не знайдено", + "ca": "Conversa no trobada" }, "CONVERSATION$NO_HISTORY_AVAILABLE": { "en": "No conversation history available", @@ -17309,7 +18391,8 @@ "fr": "Aucun historique de conversation disponible", "tr": "Kullanılabilir konuşma geçmişi yok", "de": "Keine Unterhaltungshistorie verfügbar", - "uk": "Історія розмов недоступна" + "uk": "Історія розмов недоступна", + "ca": "No hi ha historial de conversa disponible" }, "CONVERSATION$SHARED_CONVERSATION": { "en": "Shared Conversation", @@ -17325,7 +18408,8 @@ "fr": "Conversation publique", "tr": "Herkese açık konuşma", "de": "Öffentliche Unterhaltung", - "uk": "Публічна розмова" + "uk": "Публічна розмова", + "ca": "Conversa compartida" }, "CONVERSATION$LINK_COPIED": { "en": "Link copied to clipboard", @@ -17341,7 +18425,144 @@ "fr": "Lien copié dans le presse-papiers", "tr": "Bağlantı panoya kopyalandı", "de": "Link in die Zwischenablage kopiert", - "uk": "Посилання скопійовано в буфер обміну" + "uk": "Посилання скопійовано в буфер обміну", + "ca": "Enllaç copiat al porta-retalls" + }, + "ONBOARDING$STEP1_TITLE": { + "en": "What's your role?", + "ja": "あなたの役割は?", + "zh-CN": "您的角色是什么?", + "zh-TW": "您的角色是什麼?", + "ko-KR": "귀하의 역할은 무엇입니까?", + "no": "Hva er din rolle?", + "ar": "ما هو دورك؟", + "de": "Was ist Ihre Rolle?", + "fr": "Quel est votre rôle ?", + "it": "Qual è il tuo ruolo?", + "pt": "Qual é o seu papel?", + "es": "¿Cuál es tu rol?", + "tr": "Rolünüz nedir?", + "uk": "Яка ваша роль?", + "ca": "Quin és el vostre rol?" + }, + "ONBOARDING$STEP1_SUBTITLE": { + "en": "Select the option that best fits you", + "ja": "最も当てはまるオプションを選択してください", + "zh-CN": "选择最适合您的选项", + "zh-TW": "選擇最適合您的選項", + "ko-KR": "가장 적합한 옵션을 선택하세요", + "no": "Velg alternativet som passer deg best", + "ar": "اختر الخيار الأنسب لك", + "de": "Wählen Sie die Option, die am besten zu Ihnen passt", + "fr": "Sélectionnez l'option qui vous convient le mieux", + "it": "Seleziona l'opzione più adatta a te", + "pt": "Selecione a opção que melhor se adapta a você", + "es": "Selecciona la opción que mejor te describa", + "tr": "Size en uygun seçeneği seçin", + "uk": "Виберіть варіант, який найкраще вам підходить", + "ca": "Seleccioneu l'opció que millor us descrigui" + }, + "ONBOARDING$SOFTWARE_ENGINEER": { + "en": "Software engineer / developer", + "ja": "ソフトウェアエンジニア / 開発者", + "zh-CN": "软件工程师 / 开发者", + "zh-TW": "軟體工程師 / 開發者", + "ko-KR": "소프트웨어 엔지니어 / 개발자", + "no": "Programvareingeniør / utvikler", + "ar": "مهندس برمجيات / مطور", + "de": "Softwareentwickler / Entwickler", + "fr": "Ingénieur logiciel / développeur", + "it": "Ingegnere software / sviluppatore", + "pt": "Engenheiro de software / desenvolvedor", + "es": "Ingeniero de software / desarrollador", + "tr": "Yazılım mühendisi / geliştirici", + "uk": "Програмний інженер / розробник", + "ca": "Enginyer/a de programari / desenvolupador/a" + }, + "ONBOARDING$ENGINEERING_MANAGER": { + "en": "Engineering manager / tech lead", + "ja": "エンジニアリングマネージャー / テックリード", + "zh-CN": "工程经理 / 技术负责人", + "zh-TW": "工程經理 / 技術負責人", + "ko-KR": "엔지니어링 매니저 / 테크 리드", + "no": "Ingeniørsjef / teknisk leder", + "ar": "مدير هندسة / قائد تقني", + "de": "Engineering Manager / Tech Lead", + "fr": "Responsable ingénierie / tech lead", + "it": "Engineering manager / tech lead", + "pt": "Gerente de engenharia / tech lead", + "es": "Gerente de ingeniería / tech lead", + "tr": "Mühendislik müdürü / teknik lider", + "uk": "Менеджер з розробки / технічний лідер", + "ca": "Responsable d'enginyeria / líder tècnic/a" + }, + "ONBOARDING$CTO_FOUNDER": { + "en": "CTO / founder", + "ja": "CTO / 創業者", + "zh-CN": "CTO / 创始人", + "zh-TW": "CTO / 創辦人", + "ko-KR": "CTO / 창업자", + "no": "CTO / grunnlegger", + "ar": "مدير التكنولوجيا / مؤسس", + "de": "CTO / Gründer", + "fr": "CTO / fondateur", + "it": "CTO / fondatore", + "pt": "CTO / fundador", + "es": "CTO / fundador", + "tr": "CTO / kurucu", + "uk": "CTO / засновник", + "ca": "CTO / fundador/a" + }, + "ONBOARDING$PRODUCT_OPERATIONS": { + "en": "Product or operations role", + "ja": "プロダクトまたはオペレーションの役割", + "zh-CN": "产品或运营角色", + "zh-TW": "產品或營運角色", + "ko-KR": "제품 또는 운영 역할", + "no": "Produkt- eller driftsrolle", + "ar": "دور المنتج أو العمليات", + "de": "Produkt- oder Betriebsrolle", + "fr": "Rôle produit ou opérations", + "it": "Ruolo prodotto o operazioni", + "pt": "Função de produto ou operações", + "es": "Rol de producto u operaciones", + "tr": "Ürün veya operasyon rolü", + "uk": "Роль продукту або операцій", + "ca": "Rol de producte o operacions" + }, + "ONBOARDING$STUDENT_HOBBYIST": { + "en": "Student / hobbyist", + "ja": "学生 / 趣味", + "zh-CN": "学生 / 爱好者", + "zh-TW": "學生 / 愛好者", + "ko-KR": "학생 / 취미", + "no": "Student / hobbyist", + "ar": "طالب / هاوٍ", + "de": "Student / Hobbyist", + "fr": "Étudiant / amateur", + "it": "Studente / hobbista", + "pt": "Estudante / hobbyista", + "es": "Estudiante / aficionado", + "tr": "Öğrenci / hobi", + "uk": "Студент / хобіст", + "ca": "Estudiant / aficionat/da" + }, + "ONBOARDING$OTHER": { + "en": "Other", + "ja": "その他", + "zh-CN": "其他", + "zh-TW": "其他", + "ko-KR": "기타", + "no": "Annet", + "ar": "أخرى", + "de": "Andere", + "fr": "Autre", + "it": "Altro", + "pt": "Outro", + "es": "Otro", + "tr": "Diğer", + "uk": "Інше", + "ca": "Altre" }, "HOOKS_MODAL$TITLE": { "en": "Available Hooks", @@ -17357,7 +18578,8 @@ "pt": "Hooks disponíveis", "es": "Hooks disponibles", "tr": "Kullanılabilir kancalar", - "uk": "Доступні хуки" + "uk": "Доступні хуки", + "ca": "Hooks disponibles" }, "HOOKS_MODAL$WARNING": { "en": "Hooks are loaded from your workspace. This view refreshes on demand and may differ from the hooks that were active when the conversation started. Stop and restart the conversation to apply changes.", @@ -17373,7 +18595,8 @@ "pt": "Os hooks são carregados do seu workspace. Esta visualização é atualizada sob demanda a partir do workspace e pode ser diferente dos hooks que estavam ativos quando a conversa foi iniciada. Pare e reinicie a conversa para aplicar as alterações.", "es": "Los hooks se cargan desde tu espacio de trabajo. Esta vista se actualiza bajo demanda desde el workspace y puede diferir de los hooks que estaban activos cuando comenzó la conversación. Detén y reinicia la conversación para aplicar los cambios.", "tr": "Kancalar çalışma alanınızdan yüklenir. Bu görünüm istek üzerine çalışma alanından yenilenir ve sohbet başlatıldığında etkin olan kancalardan farklı olabilir. Değişiklikleri uygulamak için sohbeti durdurup yeniden başlatın.", - "uk": "Хуки завантажуються з вашого робочого простору. Це подання оновлюється з робочого простору на вимогу й може відрізнятися від хуків, які були активні під час запуску розмови. Щоб застосувати зміни, зупиніть і перезапустіть розмову." + "uk": "Хуки завантажуються з вашого робочого простору. Це подання оновлюється з робочого простору на вимогу й може відрізнятися від хуків, які були активні під час запуску розмови. Щоб застосувати зміни, зупиніть і перезапустіть розмову.", + "ca": "Els hooks es carreguen des del vostre espai de treball. Aquesta vista s'actualitza a demanda i pot diferir dels hooks que estaven actius quan va començar la conversa. Atureu i reinicieu la conversa per aplicar els canvis." }, "HOOKS_MODAL$MATCHER": { "en": "Matcher", @@ -17389,7 +18612,8 @@ "pt": "Matcher", "es": "Matcher", "tr": "Eşleştirici", - "uk": "Матчер" + "uk": "Матчер", + "ca": "Coincidència" }, "HOOKS_MODAL$COMMANDS": { "en": "Commands", @@ -17405,7 +18629,8 @@ "pt": "Comandos", "es": "Comandos", "tr": "Komutlar", - "uk": "Команди" + "uk": "Команди", + "ca": "Comandes" }, "HOOKS_MODAL$HOOK_COUNT": { "en": "{{count}} hook(s)", @@ -17421,7 +18646,8 @@ "pt": "{{count}} hook", "es": "{{count}} hook", "tr": "{{count}} kanca", - "uk": "{{count}} хук" + "uk": "{{count}} хук", + "ca": "{{count}} hook(s)" }, "HOOKS_MODAL$TYPE": { "en": "Type: {{type}}", @@ -17437,7 +18663,8 @@ "pt": "Tipo: {{type}}", "es": "Tipo: {{type}}", "tr": "Tür: {{type}}", - "uk": "Тип: {{type}}" + "uk": "Тип: {{type}}", + "ca": "Tipus: {{type}}" }, "HOOKS_MODAL$TIMEOUT": { "en": "Timeout: {{timeout}}s", @@ -17453,7 +18680,8 @@ "pt": "Tempo limite: {{timeout}}s", "es": "Tiempo de espera: {{timeout}}s", "tr": "Zaman aşımı: {{timeout}}s", - "uk": "Таймаут: {{timeout}}с" + "uk": "Таймаут: {{timeout}}с", + "ca": "Temps d'espera: {{timeout}}s" }, "HOOKS_MODAL$ASYNC": { "en": "Async", @@ -17469,7 +18697,8 @@ "pt": "Assíncrono", "es": "Asíncrono", "tr": "Asenkron", - "uk": "Асинхронний" + "uk": "Асинхронний", + "ca": "Asíncron" }, "HOOKS_MODAL$EVENT_PRE_TOOL_USE": { "en": "Pre Tool Use", @@ -17485,7 +18714,8 @@ "pt": "Antes do uso da ferramenta", "es": "Antes del uso de la herramienta", "tr": "Araç kullanımı öncesi", - "uk": "Перед використанням інструменту" + "uk": "Перед використанням інструменту", + "ca": "Abans d'usar l'eina" }, "HOOKS_MODAL$EVENT_POST_TOOL_USE": { "en": "Post Tool Use", @@ -17501,7 +18731,8 @@ "pt": "Após o uso da ferramenta", "es": "Después del uso de la herramienta", "tr": "Araç kullanımı sonrası", - "uk": "Після використання інструменту" + "uk": "Після використання інструменту", + "ca": "Després d'usar l'eina" }, "HOOKS_MODAL$EVENT_USER_PROMPT_SUBMIT": { "en": "User Prompt Submit", @@ -17517,7 +18748,8 @@ "pt": "Envio de prompt do usuário", "es": "Envío de solicitud del usuario", "tr": "Kullanıcı istemi gönderimi", - "uk": "Надсилання запиту користувача" + "uk": "Надсилання запиту користувача", + "ca": "Enviament del missatge de l'usuari" }, "HOOKS_MODAL$EVENT_SESSION_START": { "en": "Session Start", @@ -17533,7 +18765,8 @@ "pt": "Início da sessão", "es": "Inicio de sesión", "tr": "Oturum başlangıcı", - "uk": "Початок сесії" + "uk": "Початок сесії", + "ca": "Inici de sessió" }, "HOOKS_MODAL$EVENT_SESSION_END": { "en": "Session End", @@ -17549,7 +18782,8 @@ "pt": "Fim da sessão", "es": "Fin de sesión", "tr": "Oturum sonu", - "uk": "Кінець сесії" + "uk": "Кінець сесії", + "ca": "Fi de sessió" }, "HOOKS_MODAL$EVENT_STOP": { "en": "Stop", @@ -17565,7 +18799,8 @@ "pt": "Parar", "es": "Detener", "tr": "Durdur", - "uk": "Зупинка" + "uk": "Зупинка", + "ca": "Atura" }, "HOOK$HOOK_LABEL": { "en": "Hook", @@ -17581,7 +18816,8 @@ "pt": "Hook", "es": "Gancho", "tr": "Kanca", - "uk": "Хук" + "uk": "Хук", + "ca": "Hook" }, "HOOK$COMMAND": { "en": "Command", @@ -17597,7 +18833,8 @@ "pt": "Comando", "es": "Comando", "tr": "Komut", - "uk": "Команда" + "uk": "Команда", + "ca": "Comanda" }, "HOOK$EXIT_CODE": { "en": "Exit code", @@ -17613,7 +18850,8 @@ "pt": "Código de saída", "es": "Código de salida", "tr": "Çıkış kodu", - "uk": "Код виходу" + "uk": "Код виходу", + "ca": "Codi de sortida" }, "HOOK$BLOCKED_REASON": { "en": "Blocked reason", @@ -17629,7 +18867,8 @@ "pt": "Motivo do bloqueio", "es": "Motivo del bloqueo", "tr": "Engelleme nedeni", - "uk": "Причина блокування" + "uk": "Причина блокування", + "ca": "Motiu de bloqueig" }, "HOOK$CONTEXT": { "en": "Context", @@ -17645,7 +18884,8 @@ "pt": "Contexto", "es": "Contexto", "tr": "Bağlam", - "uk": "Контекст" + "uk": "Контекст", + "ca": "Context" }, "HOOK$ERROR": { "en": "Error", @@ -17662,6 +18902,8 @@ "es": "Error", "tr": "Hata", "uk": "Помилка" + , + "ca": "Error" }, "HOOK$OUTPUT": { "en": "Output", @@ -17677,7 +18919,8 @@ "pt": "Saída", "es": "Salida", "tr": "Çıktı", - "uk": "Вивід" + "uk": "Вивід", + "ca": "Sortida" }, "HOOK$STDERR": { "en": "Stderr", @@ -17693,7 +18936,8 @@ "pt": "Erro padrão", "es": "Error estándar", "tr": "Standart hata", - "uk": "Стандартна помилка" + "uk": "Стандартна помилка", + "ca": "Stderr" }, "COMMON$TYPE_EMAIL_AND_PRESS_SPACE": { "en": "Type email and press Space", @@ -17709,7 +18953,8 @@ "fr": "Tapez l'e-mail et appuyez sur Espace", "tr": "E-postu yazıp Boşluk tuşuna basın", "de": "E-Mail eingeben und Leertaste drücken", - "uk": "Введіть e-mail і натисніть Пробіл" + "uk": "Введіть e-mail і натисніть Пробіл", + "ca": "Escriviu el correu electrònic i premeu Espai" }, "ORG$INVITE_ORG_MEMBERS": { "en": "Invite Organization Members", @@ -17725,7 +18970,8 @@ "fr": "Inviter des membres de l'organisation", "tr": "Organizasyon üyelerini davet et", "de": "Organisationsmitglieder einladen", - "uk": "Запросити членів організації" + "uk": "Запросити членів організації", + "ca": "Convida membres de l'organització" }, "ORG$MANAGE_ORGANIZATION": { "en": "Manage Organization", @@ -17741,7 +18987,8 @@ "fr": "Gérer l'organisation", "tr": "Organizasyonu yönet", "de": "Organisation verwalten", - "uk": "Керувати організацією" + "uk": "Керувати організацією", + "ca": "Gestiona l'organització" }, "ORG$ORGANIZATION_MEMBERS": { "en": "Organization Members", @@ -17757,7 +19004,8 @@ "fr": "Membres de l'organisation", "tr": "Organizasyon Üyeleri", "de": "Organisationsmitglieder", - "uk": "Члени організації" + "uk": "Члени організації", + "ca": "Membres de l'organització" }, "ORG$ALL_ORGANIZATION_MEMBERS": { "en": "All Organization Members", @@ -17773,7 +19021,8 @@ "fr": "Tous les membres de l'organisation", "tr": "Tüm organizasyon üyeleri", "de": "Alle Organisationsmitglieder", - "uk": "Усі члени організації" + "uk": "Усі члени організації", + "ca": "Tots els membres de l'organització" }, "ORG$SEARCH_BY_EMAIL": { "en": "Search by email...", @@ -17789,7 +19038,8 @@ "fr": "Rechercher par e-mail...", "tr": "E-posta ile ara...", "de": "Nach E-Mail suchen...", - "uk": "Пошук за електронною поштою..." + "uk": "Пошук за електронною поштою...", + "ca": "Cerca per correu electrònic..." }, "ORG$NO_MEMBERS_FOUND": { "en": "No members found", @@ -17805,7 +19055,8 @@ "fr": "Aucun membre trouvé", "tr": "Üye bulunamadı", "de": "Keine Mitglieder gefunden", - "uk": "Членів не знайдено" + "uk": "Членів не знайдено", + "ca": "No s'han trobat membres" }, "ORG$NO_MEMBERS_MATCHING_FILTER": { "en": "No members match your search", @@ -17821,7 +19072,8 @@ "fr": "Aucun membre ne correspond à votre recherche", "tr": "Aramanızla eşleşen üye bulunamadı", "de": "Keine Mitglieder entsprechen Ihrer Suche", - "uk": "Жодний член не відповідає вашому пошуку" + "uk": "Жодний член не відповідає вашому пошуку", + "ca": "Cap membre coincideix amb la vostra cerca" }, "ORG$FAILED_TO_LOAD_MEMBERS": { "en": "Failed to load members", @@ -17837,7 +19089,8 @@ "fr": "Échec du chargement des membres", "tr": "Üyeler yüklenemedi", "de": "Mitglieder konnten nicht geladen werden", - "uk": "Не вдалося завантажити членів" + "uk": "Не вдалося завантажити членів", + "ca": "No s'han pogut carregar els membres" }, "ONBOARDING$ORG_NAME_QUESTION": { "en": "What's the name of your organization?", @@ -17853,7 +19106,8 @@ "pt": "Qual é o nome da sua organização?", "es": "¿Cuál es el nombre de tu organización?", "tr": "Organizasyonunuzun adı nedir?", - "uk": "Як називається ваша організація?" + "uk": "Як називається ваша організація?", + "ca": "Com es diu la vostra organització?" }, "ONBOARDING$ORG_NAME_INPUT_NAME": { "en": "Org name", @@ -17870,6 +19124,8 @@ "es": "Nombre de la organización", "tr": "Organizasyon adı", "uk": "Назва організації" + , + "ca": "Nom de l'organització" }, "ONBOARDING$ORG_NAME_INPUT_DOMAIN": { "en": "Domain name", @@ -17885,7 +19141,8 @@ "pt": "Nome de domínio", "es": "Nombre de dominio", "tr": "Alan adı", - "uk": "Доменне ім'я" + "uk": "Доменне ім'я", + "ca": "Nom de domini" }, "ONBOARDING$ORG_SIZE_QUESTION": { "en": "What size organization do you work for?", @@ -17901,7 +19158,8 @@ "pt": "Qual o tamanho da organização em que você trabalha?", "es": "¿De qué tamaño es la organización para la que trabajas?", "tr": "Hangi büyüklükte bir organizasyon için çalışıyorsunuz?", - "uk": "Якого розміру організація, в якій ви працюєте?" + "uk": "Якого розміру організація, в якій ви працюєте?", + "ca": "De quina mida és la vostra organització?" }, "ONBOARDING$ORG_SIZE_SUBTITLE": { "en": "Select one", @@ -17917,7 +19175,8 @@ "pt": "Selecione uma opção", "es": "Seleccione una opción", "tr": "Bir seçenek seçin", - "uk": "Виберіть один" + "uk": "Виберіть один", + "ca": "Seleccioneu-ne un" }, "ONBOARDING$ORG_SIZE_SOLO": { "en": "Just me (solo)", @@ -17933,7 +19192,8 @@ "pt": "Apenas eu (solo)", "es": "Solo yo (individual)", "tr": "Sadece ben (solo)", - "uk": "Тільки я (соло)" + "uk": "Тільки я (соло)", + "ca": "Només jo (individual)" }, "ONBOARDING$ORG_SIZE_2_10": { "en": "2–10 people", @@ -17949,7 +19209,8 @@ "pt": "2–10 pessoas", "es": "2–10 personas", "tr": "2–10 kişi", - "uk": "2–10 осіб" + "uk": "2–10 осіб", + "ca": "2-10 persones" }, "ONBOARDING$ORG_SIZE_11_50": { "en": "11–50 people", @@ -17965,7 +19226,8 @@ "pt": "11–50 pessoas", "es": "11–50 personas", "tr": "11–50 kişi", - "uk": "11–50 осіб" + "uk": "11–50 осіб", + "ca": "11-50 persones" }, "ONBOARDING$ORG_SIZE_51_200": { "en": "51–200 people", @@ -17981,7 +19243,8 @@ "pt": "51–200 pessoas", "es": "51–200 personas", "tr": "51–200 kişi", - "uk": "51–200 осіб" + "uk": "51–200 осіб", + "ca": "51-200 persones" }, "ONBOARDING$ORG_SIZE_200_PLUS": { "en": "200+ people", @@ -17997,7 +19260,8 @@ "pt": "200+ pessoas", "es": "200+ personas", "tr": "200+ kişi", - "uk": "200+ осіб" + "uk": "200+ осіб", + "ca": "Més de 200 persones" }, "ONBOARDING$USE_CASE_QUESTION": { "en": "What use cases are you looking to use OpenHands for?", @@ -18013,7 +19277,8 @@ "pt": "Para quais casos de uso você pretende usar o OpenHands?", "es": "¿Para qué casos de uso quieres usar OpenHands?", "tr": "OpenHands'i hangi kullanım alanları için kullanmak istiyorsunuz?", - "uk": "Для яких випадків використання ви хочете використовувати OpenHands?" + "uk": "Для яких випадків використання ви хочете використовувати OpenHands?", + "ca": "Per a quins casos d'ús voleu fer servir OpenHands?" }, "ONBOARDING$USE_CASE_SUBTITLE": { "en": "Check all that apply", @@ -18029,7 +19294,8 @@ "pt": "Selecione todas as opções aplicáveis", "es": "Selecciona todas las que apliquen", "tr": "Geçerli olanların tümünü seçin", - "uk": "Виберіть усі, що стосуються" + "uk": "Виберіть усі, що стосуються", + "ca": "Marqueu tot el que correspongui" }, "ONBOARDING$USE_CASE_NEW_FEATURES": { "en": "Writing new features to existing products", @@ -18045,7 +19311,8 @@ "pt": "Escrever novos recursos para produtos existentes", "es": "Escribir nuevas funcionalidades para productos existentes", "tr": "Mevcut ürünlere yeni özellikler yazmak", - "uk": "Написання нових функцій для існуючих продуктів" + "uk": "Написання нових функцій для існуючих продуктів", + "ca": "Escriure noves funcions per a productes existents" }, "ONBOARDING$USE_CASE_APP_FROM_SCRATCH": { "en": "Starting an app from scratch", @@ -18061,7 +19328,8 @@ "pt": "Iniciar um aplicativo do zero", "es": "Comenzar una aplicación desde cero", "tr": "Sıfırdan bir uygulama başlatmak", - "uk": "Створення додатку з нуля" + "uk": "Створення додатку з нуля", + "ca": "Iniciar una aplicació des de zero" }, "ONBOARDING$USE_CASE_FIXING_BUGS": { "en": "Fixing bugs", @@ -18077,7 +19345,8 @@ "pt": "Corrigir bugs", "es": "Corregir errores", "tr": "Hataları düzeltmek", - "uk": "Виправлення помилок" + "uk": "Виправлення помилок", + "ca": "Corregir errors" }, "ONBOARDING$USE_CASE_REFACTORING": { "en": "Refactoring existing code / eliminating tech debt", @@ -18093,7 +19362,8 @@ "pt": "Refatorar código existente / eliminar dívida técnica", "es": "Refactorizar código existente / eliminar deuda técnica", "tr": "Mevcut kodu yeniden düzenlemek / teknik borcu ortadan kaldırmak", - "uk": "Рефакторинг існуючого коду / усунення технічного боргу" + "uk": "Рефакторинг існуючого коду / усунення технічного боргу", + "ca": "Refactoritzar el codi existent / eliminar el deute tècnic" }, "ONBOARDING$USE_CASE_AUTOMATING_TASKS": { "en": "Automating repetitive coding tasks", @@ -18109,7 +19379,8 @@ "pt": "Automatizar tarefas de codificação repetitivas", "es": "Automatizar tareas de codificación repetitivas", "tr": "Tekrarlayan kodlama görevlerini otomatikleştirmek", - "uk": "Автоматизація повторюваних завдань кодування" + "uk": "Автоматизація повторюваних завдань кодування", + "ca": "Automatitzar tasques de codificació repetitives" }, "ONBOARDING$USE_CASE_NOT_SURE": { "en": "Not sure yet", @@ -18125,7 +19396,8 @@ "pt": "Ainda não tenho certeza", "es": "Aún no estoy seguro", "tr": "Henüz emin değilim", - "uk": "Ще не впевнений" + "uk": "Ще не впевнений", + "ca": "Encara no ho sé" }, "ONBOARDING$ROLE_QUESTION": { "en": "What's your role?", @@ -18141,7 +19413,8 @@ "pt": "Qual é o seu papel?", "es": "¿Cuál es tu rol?", "tr": "Rolünüz nedir?", - "uk": "Яка ваша роль?" + "uk": "Яка ваша роль?", + "ca": "Quin és el vostre rol?" }, "ONBOARDING$ROLE_SOFTWARE_ENGINEER": { "en": "Software engineer / developer", @@ -18157,7 +19430,8 @@ "pt": "Engenheiro de software / desenvolvedor", "es": "Ingeniero de software / desarrollador", "tr": "Yazılım mühendisi / geliştirici", - "uk": "Програмний інженер / розробник" + "uk": "Програмний інженер / розробник", + "ca": "Enginyer/a de programari / desenvolupador/a" }, "ONBOARDING$ROLE_ENGINEERING_MANAGER": { "en": "Engineering manager / tech lead", @@ -18173,7 +19447,8 @@ "pt": "Gerente de engenharia / tech lead", "es": "Gerente de ingeniería / tech lead", "tr": "Mühendislik müdürü / teknik lider", - "uk": "Менеджер з розробки / технічний лідер" + "uk": "Менеджер з розробки / технічний лідер", + "ca": "Responsable d'enginyeria / líder tècnic/a" }, "ONBOARDING$ROLE_CTO_FOUNDER": { "en": "CTO / founder", @@ -18189,7 +19464,8 @@ "pt": "CTO / fundador", "es": "CTO / fundador", "tr": "CTO / kurucu", - "uk": "CTO / засновник" + "uk": "CTO / засновник", + "ca": "CTO / fundador/a" }, "ONBOARDING$ROLE_PRODUCT_OPERATIONS": { "en": "Product or operations role", @@ -18205,7 +19481,8 @@ "pt": "Função de produto ou operações", "es": "Rol de producto u operaciones", "tr": "Ürün veya operasyon rolü", - "uk": "Роль продукту або операцій" + "uk": "Роль продукту або операцій", + "ca": "Rol de producte o operacions" }, "ONBOARDING$ROLE_STUDENT_HOBBYIST": { "en": "Student / hobbyist", @@ -18221,7 +19498,8 @@ "pt": "Estudante / hobbyista", "es": "Estudiante / aficionado", "tr": "Öğrenci / hobi", - "uk": "Студент / хобіст" + "uk": "Студент / хобіст", + "ca": "Estudiant / aficionat/da" }, "ONBOARDING$ROLE_OTHER": { "en": "Other", @@ -18237,7 +19515,8 @@ "pt": "Outro", "es": "Otro", "tr": "Diğer", - "uk": "Інше" + "uk": "Інше", + "ca": "Altre" }, "ONBOARDING$NEXT_BUTTON": { "en": "Next", @@ -18253,7 +19532,8 @@ "pt": "Próximo", "es": "Siguiente", "tr": "İleri", - "uk": "Далі" + "uk": "Далі", + "ca": "Següent" }, "ONBOARDING$BACK_BUTTON": { "en": "Back", @@ -18269,7 +19549,8 @@ "pt": "Voltar", "es": "Atrás", "tr": "Geri", - "uk": "Назад" + "uk": "Назад", + "ca": "Enrere" }, "ONBOARDING$FINISH_BUTTON": { "en": "Finish", @@ -18285,7 +19566,8 @@ "pt": "Concluir", "es": "Finalizar", "tr": "Bitir", - "uk": "Завершити" + "uk": "Завершити", + "ca": "Finalitza" }, "CTA$ENTERPRISE": { "en": "Enterprise", @@ -18301,7 +19583,8 @@ "pt": "Empresarial", "es": "Empresa", "tr": "Kurumsal", - "uk": "Підприємство" + "uk": "Підприємство", + "ca": "Empresa" }, "CTA$ENTERPRISE_DEPLOY": { "en": "Deploy OpenHands on your own infrastructure. Full control over data, compliance, and security.", @@ -18317,7 +19600,8 @@ "pt": "Implante o OpenHands em sua própria infraestrutura. Controle total sobre dados, conformidade e segurança.", "es": "Implemente OpenHands en su propia infraestructura. Control total sobre datos, cumplimiento y seguridad.", "tr": "OpenHands'i kendi altyapınızda dağıtın. Veri, uyumluluk ve güvenlik üzerinde tam kontrol.", - "uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над даними, відповідністю та безпекою." + "uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над даними, відповідністю та безпекою.", + "ca": "Desplegeu OpenHands a la vostra pròpia infraestructura. Control total sobre dades, compliment i seguretat." }, "CTA$FEATURE_ON_PREMISES": { "en": "On-premises or private cloud", @@ -18333,7 +19617,8 @@ "pt": "Local ou nuvem privada", "es": "Local o nube privada", "tr": "Şirket içi veya özel bulut", - "uk": "Локально або приватна хмара" + "uk": "Локально або приватна хмара", + "ca": "Local o núvol privat" }, "CTA$FEATURE_DATA_CONTROL": { "en": "Full data control", @@ -18349,7 +19634,8 @@ "pt": "Controle total de dados", "es": "Control total de datos", "tr": "Tam veri kontrolü", - "uk": "Повний контроль даних" + "uk": "Повний контроль даних", + "ca": "Control total de les dades" }, "CTA$FEATURE_COMPLIANCE": { "en": "Custom compliance requirements", @@ -18365,7 +19651,8 @@ "pt": "Requisitos de conformidade personalizados", "es": "Requisitos de cumplimiento personalizados", "tr": "Özel uyumluluk gereksinimleri", - "uk": "Індивідуальні вимоги відповідності" + "uk": "Індивідуальні вимоги відповідності", + "ca": "Requisits de compliment personalitzats" }, "CTA$FEATURE_SUPPORT": { "en": "Dedicated support options", @@ -18381,7 +19668,8 @@ "pt": "Opções de suporte dedicado", "es": "Opciones de soporte dedicado", "tr": "Özel destek seçenekleri", - "uk": "Виділені варіанти підтримки" + "uk": "Виділені варіанти підтримки", + "ca": "Opcions de suport dedicat" }, "ENTERPRISE$SELF_HOSTED": { "en": "Self-Hosted", @@ -18397,7 +19685,8 @@ "pt": "Auto-hospedado", "es": "Autoalojado", "tr": "Kendi Sunucunuzda", - "uk": "Самостійний хостинг" + "uk": "Самостійний хостинг", + "ca": "Allotjament propi" }, "ENTERPRISE$TITLE": { "en": "OpenHands Enterprise", @@ -18413,7 +19702,8 @@ "pt": "OpenHands Enterprise", "es": "OpenHands Enterprise", "tr": "OpenHands Kurumsal", - "uk": "OpenHands Enterprise" + "uk": "OpenHands Enterprise", + "ca": "OpenHands Enterprise" }, "ENTERPRISE$DESCRIPTION": { "en": "Complete data control with your own self-hosted AI development platform.", @@ -18429,7 +19719,8 @@ "pt": "Controle completo de dados com sua própria plataforma de desenvolvimento de IA auto-hospedada.", "es": "Control completo de datos con tu propia plataforma de desarrollo de IA autoalojada.", "tr": "Kendi barındırdığınız yapay zeka geliştirme platformuyla tam veri kontrolü.", - "uk": "Повний контроль над даними з власною самостійно розміщеною платформою розробки ШІ." + "uk": "Повний контроль над даними з власною самостійно розміщеною платформою розробки ШІ.", + "ca": "Control total de les dades amb la vostra pròpia plataforma de desenvolupament d'IA allotjada." }, "ENTERPRISE$FEATURE_DATA_PRIVACY": { "en": "Full data privacy & control", @@ -18445,7 +19736,8 @@ "pt": "Privacidade e controle total de dados", "es": "Privacidad y control total de datos", "tr": "Tam veri gizliliği ve kontrolü", - "uk": "Повна конфіденційність та контроль даних" + "uk": "Повна конфіденційність та контроль даних", + "ca": "Privacitat i control total de les dades" }, "ENTERPRISE$FEATURE_DEPLOYMENT": { "en": "Custom deployment options", @@ -18461,7 +19753,8 @@ "pt": "Opções de implantação personalizadas", "es": "Opciones de despliegue personalizadas", "tr": "Özel dağıtım seçenekleri", - "uk": "Налаштовані варіанти розгортання" + "uk": "Налаштовані варіанти розгортання", + "ca": "Opcions de desplegament personalitzades" }, "ENTERPRISE$FEATURE_SSO": { "en": "SSO & enterprise auth", @@ -18477,7 +19770,8 @@ "pt": "SSO e autenticação empresarial", "es": "SSO y autenticación empresarial", "tr": "SSO ve kurumsal kimlik doğrulama", - "uk": "SSO та корпоративна автентифікація" + "uk": "SSO та корпоративна автентифікація", + "ca": "SSO i autenticació empresarial" }, "ENTERPRISE$FEATURE_SUPPORT": { "en": "Dedicated support", @@ -18493,7 +19787,8 @@ "pt": "Suporte dedicado", "es": "Soporte dedicado", "tr": "Özel destek", - "uk": "Виділена підтримка" + "uk": "Виділена підтримка", + "ca": "Suport dedicat" }, "ENTERPRISE$LEARN_MORE": { "en": "Learn More", @@ -18510,6 +19805,8 @@ "es": "Más información", "tr": "Daha Fazla Bilgi", "uk": "Дізнатися більше" + , + "ca": "Més informació" }, "ENTERPRISE$LEARN_MORE_ARIA": { "en": "Learn more about OpenHands Enterprise (opens in new window)", @@ -18525,7 +19822,8 @@ "pt": "Saiba mais sobre OpenHands Enterprise (abre em nova janela)", "es": "Más información sobre OpenHands Enterprise (abre en nueva ventana)", "tr": "OpenHands Enterprise hakkında daha fazla bilgi edinin (yeni pencerede açılır)", - "uk": "Дізнатися більше про OpenHands Enterprise (відкривається в новому вікні)" + "uk": "Дізнатися більше про OpenHands Enterprise (відкривається в новому вікні)", + "ca": "Més informació sobre OpenHands Enterprise (s'obre en una finestra nova)" }, "DEVICE$SUCCESS_TITLE": { "en": "Success!", @@ -18541,7 +19839,8 @@ "pt": "Sucesso!", "es": "¡Éxito!", "tr": "Başarılı!", - "uk": "Успіх!" + "uk": "Успіх!", + "ca": "Correcte!" }, "DEVICE$ERROR_TITLE": { "en": "Error", @@ -18558,6 +19857,8 @@ "es": "Error", "tr": "Hata", "uk": "Помилка" + , + "ca": "Error" }, "DEVICE$SUCCESS_MESSAGE": { "en": "Device authorized successfully! You can now return to your CLI and close this window.", @@ -18573,7 +19874,8 @@ "pt": "Dispositivo autorizado com sucesso! Você pode voltar ao CLI e fechar esta janela.", "es": "¡Dispositivo autorizado exitosamente! Ahora puedes volver a tu CLI y cerrar esta ventana.", "tr": "Cihaz başarıyla yetkilendirildi! Artık CLI'nize dönebilir ve bu pencereyi kapatabilirsiniz.", - "uk": "Пристрій успішно авторизовано! Тепер ви можете повернутися до CLI та закрити це вікно." + "uk": "Пристрій успішно авторизовано! Тепер ви можете повернутися до CLI та закрити це вікно.", + "ca": "Dispositiu autoritzat correctament. Ja podeu tornar a la vostra CLI i tancar aquesta finestra." }, "DEVICE$ERROR_FAILED": { "en": "Failed to authorize device. Please try again.", @@ -18589,7 +19891,8 @@ "pt": "Falha ao autorizar o dispositivo. Por favor, tente novamente.", "es": "Error al autorizar el dispositivo. Por favor, inténtalo de nuevo.", "tr": "Cihaz yetkilendirilemedi. Lütfen tekrar deneyin.", - "uk": "Не вдалося авторизувати пристрій. Будь ласка, спробуйте ще раз." + "uk": "Не вдалося авторизувати пристрій. Будь ласка, спробуйте ще раз.", + "ca": "No s'ha pogut autoritzar el dispositiu. Torneu-ho a intentar." }, "DEVICE$ERROR_OCCURRED": { "en": "An error occurred while authorizing the device. Please try again.", @@ -18605,7 +19908,8 @@ "pt": "Ocorreu um erro ao autorizar o dispositivo. Por favor, tente novamente.", "es": "Ocurrió un error al autorizar el dispositivo. Por favor, inténtalo de nuevo.", "tr": "Cihaz yetkilendirirken bir hata oluştu. Lütfen tekrar deneyin.", - "uk": "Під час авторизації пристрою сталася помилка. Будь ласка, спробуйте ще раз." + "uk": "Під час авторизації пристрою сталася помилка. Будь ласка, спробуйте ще раз.", + "ca": "S'ha produït un error en autoritzar el dispositiu. Torneu-ho a intentar." }, "DEVICE$TRY_AGAIN": { "en": "Try Again", @@ -18621,7 +19925,8 @@ "pt": "Tentar novamente", "es": "Intentar de nuevo", "tr": "Tekrar Dene", - "uk": "Спробувати ще раз" + "uk": "Спробувати ще раз", + "ca": "Torna-ho a intentar" }, "DEVICE$PROCESSING": { "en": "Processing device verification...", @@ -18637,7 +19942,8 @@ "pt": "Processando verificação do dispositivo...", "es": "Procesando verificación del dispositivo...", "tr": "Cihaz doğrulaması işleniyor...", - "uk": "Обробка перевірки пристрою..." + "uk": "Обробка перевірки пристрою...", + "ca": "S'està processant la verificació del dispositiu..." }, "DEVICE$AUTHORIZATION_REQUEST": { "en": "Device Authorization Request", @@ -18653,7 +19959,8 @@ "pt": "Solicitação de autorização do dispositivo", "es": "Solicitud de autorización del dispositivo", "tr": "Cihaz Yetkilendirme Talebi", - "uk": "Запит на авторизацію пристрою" + "uk": "Запит на авторизацію пристрою", + "ca": "Sol·licitud d'autorització del dispositiu" }, "DEVICE$CODE_LABEL": { "en": "DEVICE CODE", @@ -18669,7 +19976,8 @@ "pt": "CÓDIGO DO DISPOSITIVO", "es": "CÓDIGO DE DISPOSITIVO", "tr": "CİHAZ KODU", - "uk": "КОД ПРИСТРОЮ" + "uk": "КОД ПРИСТРОЮ", + "ca": "CODI DEL DISPOSITIU" }, "DEVICE$SECURITY_NOTICE": { "en": "Security Notice", @@ -18685,7 +19993,8 @@ "pt": "Aviso de segurança", "es": "Aviso de seguridad", "tr": "Güvenlik Bildirimi", - "uk": "Повідомлення про безпеку" + "uk": "Повідомлення про безпеку", + "ca": "Avís de seguretat" }, "DEVICE$SECURITY_WARNING": { "en": "Only authorize this device if you initiated this request from your CLI or application.", @@ -18701,7 +20010,8 @@ "pt": "Autorize este dispositivo apenas se você iniciou esta solicitação do seu CLI ou aplicativo.", "es": "Solo autoriza este dispositivo si iniciaste esta solicitud desde tu CLI o aplicación.", "tr": "Bu cihazı yalnızca bu isteği CLI veya uygulamanızdan başlattıysanız yetkilendirin.", - "uk": "Авторизуйте цей пристрій лише якщо ви ініціювали цей запит з вашого CLI або додатку." + "uk": "Авторизуйте цей пристрій лише якщо ви ініціювали цей запит з вашого CLI або додатку.", + "ca": "Només autoritzi aquest dispositiu si vau iniciar aquesta sol·licitud des de la vostra CLI o aplicació." }, "DEVICE$CONFIRM_PROMPT": { "en": "Do you want to authorize this device to access your OpenHands account?", @@ -18717,7 +20027,8 @@ "pt": "Deseja autorizar este dispositivo a acessar sua conta OpenHands?", "es": "¿Deseas autorizar este dispositivo para acceder a tu cuenta de OpenHands?", "tr": "Bu cihazın OpenHands hesabınıza erişmesine izin vermek istiyor musunuz?", - "uk": "Бажаєте авторизувати цей пристрій для доступу до вашого облікового запису OpenHands?" + "uk": "Бажаєте авторизувати цей пристрій для доступу до вашого облікового запису OpenHands?", + "ca": "Voleu autoritzar aquest dispositiu per accedir al vostre compte d'OpenHands?" }, "DEVICE$CANCEL": { "en": "Cancel", @@ -18733,7 +20044,8 @@ "pt": "Cancelar", "es": "Cancelar", "tr": "İptal", - "uk": "Скасувати" + "uk": "Скасувати", + "ca": "Cancel·la" }, "DEVICE$AUTHORIZE": { "en": "Authorize Device", @@ -18749,7 +20061,8 @@ "pt": "Autorizar dispositivo", "es": "Autorizar dispositivo", "tr": "Cihazı Yetkilendir", - "uk": "Авторизувати пристрій" + "uk": "Авторизувати пристрій", + "ca": "Autoritza el dispositiu" }, "DEVICE$AUTHORIZATION_TITLE": { "en": "Device Authorization", @@ -18765,7 +20078,8 @@ "pt": "Autorização do dispositivo", "es": "Autorización del dispositivo", "tr": "Cihaz Yetkilendirme", - "uk": "Авторизація пристрою" + "uk": "Авторизація пристрою", + "ca": "Autorització del dispositiu" }, "DEVICE$ENTER_CODE_PROMPT": { "en": "Enter the code displayed on your device:", @@ -18781,7 +20095,8 @@ "pt": "Digite o código exibido no seu dispositivo:", "es": "Ingresa el código mostrado en tu dispositivo:", "tr": "Cihazınızda görüntülenen kodu girin:", - "uk": "Введіть код, відображений на вашому пристрої:" + "uk": "Введіть код, відображений на вашому пристрої:", + "ca": "Introduïu el codi que es mostra al vostre dispositiu:" }, "DEVICE$CODE_INPUT_LABEL": { "en": "Device Code:", @@ -18797,7 +20112,8 @@ "pt": "Código do dispositivo:", "es": "Código del dispositivo:", "tr": "Cihaz Kodu:", - "uk": "Код пристрою:" + "uk": "Код пристрою:", + "ca": "Codi del dispositiu:" }, "DEVICE$CODE_PLACEHOLDER": { "en": "Enter your device code", @@ -18813,7 +20129,8 @@ "pt": "Digite o código do seu dispositivo", "es": "Ingresa tu código de dispositivo", "tr": "Cihaz kodunuzu girin", - "uk": "Введіть код вашого пристрою" + "uk": "Введіть код вашого пристрою", + "ca": "Introduïu el codi del dispositiu" }, "DEVICE$CONTINUE": { "en": "Continue", @@ -18829,7 +20146,8 @@ "pt": "Continuar", "es": "Continuar", "tr": "Devam", - "uk": "Продовжити" + "uk": "Продовжити", + "ca": "Continua" }, "DEVICE$AUTH_REQUIRED": { "en": "Authentication Required", @@ -18845,7 +20163,8 @@ "pt": "Autenticação necessária", "es": "Autenticación requerida", "tr": "Kimlik Doğrulama Gerekli", - "uk": "Потрібна автентифікація" + "uk": "Потрібна автентифікація", + "ca": "Cal autenticació" }, "DEVICE$SIGN_IN_PROMPT": { "en": "Please sign in to authorize your device.", @@ -18861,7 +20180,8 @@ "pt": "Por favor, faça login para autorizar seu dispositivo.", "es": "Por favor, inicia sesión para autorizar tu dispositivo.", "tr": "Cihazınızı yetkilendirmek için lütfen giriş yapın.", - "uk": "Будь ласка, увійдіть, щоб авторизувати свій пристрій." + "uk": "Будь ласка, увійдіть, щоб авторизувати свій пристрій.", + "ca": "Inicieu sessió per autoritzar el vostre dispositiu." }, "CTA$ENTERPRISE_TITLE": { "en": "Get OpenHands For Enterprise", @@ -18877,7 +20197,8 @@ "fr": "Obtenez OpenHands pour Entreprise", "tr": "Kurumsal OpenHands'i Edinin", "de": "OpenHands für Unternehmen", - "uk": "Отримайте OpenHands для підприємств" + "uk": "Отримайте OpenHands для підприємств", + "ca": "Obteniu OpenHands per a empreses" }, "CTA$ENTERPRISE_DESCRIPTION": { "en": "Cloud allows you to access OpenHands anywhere and coordinate with your team like never before.", @@ -18893,7 +20214,8 @@ "fr": "Cloud vous permet d'accéder à OpenHands n'importe où et de coordonner avec votre équipe comme jamais auparavant.", "tr": "Cloud, OpenHands'e her yerden erişmenizi ve ekibinizle daha önce hiç olmadığı gibi koordinasyon sağlamanızı mümkün kılar.", "de": "Cloud ermöglicht Ihnen den Zugriff auf OpenHands von überall und die Koordination mit Ihrem Team wie nie zuvor.", - "uk": "Cloud дозволяє отримати доступ до OpenHands будь-де та координувати роботу з вашою командою як ніколи раніше." + "uk": "Cloud дозволяє отримати доступ до OpenHands будь-де та координувати роботу з вашою командою як ніколи раніше.", + "ca": "El núvol us permet accedir a OpenHands des de qualsevol lloc i coordinar-vos amb el vostre equip com mai abans." }, "CTA$LEARN_MORE": { "en": "Learn more", @@ -18910,5 +20232,7 @@ "tr": "Daha fazla bilgi", "de": "Mehr erfahren", "uk": "Дізнатися більше" + , + "ca": "Més informació" } } From 6d86803f4154894db19230d66c83e2d18fd3f9dd Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:26:27 -0700 Subject: [PATCH 13/21] Add loading feedback to git changes refresh button (#12792) Co-authored-by: hieptl --- frontend/__tests__/routes/changes-tab.test.tsx | 2 ++ .../conversation-tabs/conversation-tab-title.tsx | 12 +++++++++--- .../src/hooks/query/use-unified-get-git-changes.ts | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/routes/changes-tab.test.tsx b/frontend/__tests__/routes/changes-tab.test.tsx index 178bb28c40..1cf2513d18 100644 --- a/frontend/__tests__/routes/changes-tab.test.tsx +++ b/frontend/__tests__/routes/changes-tab.test.tsx @@ -32,6 +32,7 @@ describe("Changes Tab", () => { vi.mocked(useUnifiedGetGitChanges).mockReturnValue({ data: [], isLoading: false, + isFetching: false, isSuccess: true, isError: false, error: null, @@ -50,6 +51,7 @@ describe("Changes Tab", () => { vi.mocked(useUnifiedGetGitChanges).mockReturnValue({ data: [{ path: "src/file.ts", status: "M" }], isLoading: false, + isFetching: false, isSuccess: true, isError: false, error: null, diff --git a/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-title.tsx b/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-title.tsx index 75dbb23f8e..ad3bc98c41 100644 --- a/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-title.tsx +++ b/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-title.tsx @@ -20,7 +20,7 @@ export function ConversationTabTitle({ conversationKey, }: ConversationTabTitleProps) { const { t } = useTranslation(); - const { refetch } = useUnifiedGetGitChanges(); + const { refetch, isFetching } = useUnifiedGetGitChanges(); const { handleBuildPlanClick } = useHandleBuildPlanClick(); const { curAgentState } = useAgentState(); const { planContent } = useConversationStore(); @@ -41,10 +41,16 @@ export function ConversationTabTitle({ {conversationKey === "editor" && ( )} {conversationKey === "planner" && ( diff --git a/frontend/src/hooks/query/use-unified-get-git-changes.ts b/frontend/src/hooks/query/use-unified-get-git-changes.ts index 70bc5f451f..801b1a067a 100644 --- a/frontend/src/hooks/query/use-unified-get-git-changes.ts +++ b/frontend/src/hooks/query/use-unified-get-git-changes.ts @@ -100,6 +100,7 @@ export const useUnifiedGetGitChanges = () => { return { data: orderedChanges, isLoading: result.isLoading, + isFetching: result.isFetching, isSuccess: result.isSuccess, isError: result.isError, error: result.error, From 39a4ca422f35b276d8b508bf52e9fddd9ae5a0d5 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Wed, 18 Mar 2026 11:42:46 -0700 Subject: [PATCH 14/21] fix: use sentence case for 'Waiting for sandbox' text (#12958) Co-authored-by: openhands --- frontend/__tests__/utils/utils.test.ts | 4 ++-- frontend/src/utils/utils.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/__tests__/utils/utils.test.ts b/frontend/__tests__/utils/utils.test.ts index 91e9ba031b..8fc286d4b1 100644 --- a/frontend/__tests__/utils/utils.test.ts +++ b/frontend/__tests__/utils/utils.test.ts @@ -11,7 +11,7 @@ import { I18nKey } from "#/i18n/declaration"; // Mock translations const t = (key: string) => { const translations: { [key: string]: string } = { - COMMON$WAITING_FOR_SANDBOX: "Waiting For Sandbox", + COMMON$WAITING_FOR_SANDBOX: "Waiting for sandbox", COMMON$STOPPING: "Stopping", COMMON$STARTING: "Starting", COMMON$SERVER_STOPPED: "Server stopped", @@ -69,7 +69,7 @@ describe("getStatusText", () => { t, }); - expect(result).toBe(t(I18nKey.COMMON$WAITING_FOR_SANDBOX)); + expect(result).toBe("Waiting for sandbox"); }); it("returns task detail when task status is ERROR and detail exists", () => { diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 849d65fbdf..80f40158f6 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -838,7 +838,7 @@ interface GetStatusTextArgs { * isStartingStatus: false, * isStopStatus: false, * curAgentState: AgentState.RUNNING - * }) // Returns "Waiting For Sandbox" + * }) // Returns "Waiting for sandbox" */ export function getStatusText({ isPausing = false, @@ -866,13 +866,13 @@ export function getStatusText({ return t(I18nKey.CONVERSATION$READY); } - // Format status text: "WAITING_FOR_SANDBOX" -> "Waiting for sandbox" + // Format status text with sentence case: "WAITING_FOR_SANDBOX" -> "Waiting for sandbox" return ( taskDetail || taskStatus .toLowerCase() .replace(/_/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase()) + .replace(/^\w/, (c) => c.toUpperCase()) ); } From db41148396188a7566f412f1053211a655aaea7d Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:46:23 +0700 Subject: [PATCH 15/21] feat(backend): expose API key org_id via new GET /api/keys/current endpoint (org project) (#13469) --- enterprise/server/auth/saas_user_auth.py | 18 +++- enterprise/server/routes/api_keys.py | 57 ++++++++++++- enterprise/storage/api_key_store.py | 22 ++++- .../storage/saas_conversation_validator.py | 6 +- .../tests/unit/server/routes/test_api_keys.py | 85 +++++++++++++++++++ enterprise/tests/unit/test_api_key_store.py | 14 ++- enterprise/tests/unit/test_saas_user_auth.py | 17 +++- 7 files changed, 203 insertions(+), 16 deletions(-) diff --git a/enterprise/server/auth/saas_user_auth.py b/enterprise/server/auth/saas_user_auth.py index c2b3e1fbe9..6c8aefea7a 100644 --- a/enterprise/server/auth/saas_user_auth.py +++ b/enterprise/server/auth/saas_user_auth.py @@ -1,6 +1,7 @@ import time from dataclasses import dataclass from types import MappingProxyType +from uuid import UUID import jwt from fastapi import Request @@ -59,6 +60,10 @@ class SaasUserAuth(UserAuth): _secrets: Secrets | None = None accepted_tos: bool | None = None auth_type: AuthType = AuthType.COOKIE + # API key context fields - populated when authenticated via API key + api_key_org_id: UUID | None = None + api_key_id: int | None = None + api_key_name: str | None = None async def get_user_id(self) -> str | None: return self.user_id @@ -283,14 +288,19 @@ async def saas_user_auth_from_bearer(request: Request) -> SaasUserAuth | None: return None api_key_store = ApiKeyStore.get_instance() - user_id = await api_key_store.validate_api_key(api_key) - if not user_id: + validation_result = await api_key_store.validate_api_key(api_key) + if not validation_result: return None - offline_token = await token_manager.load_offline_token(user_id) + offline_token = await token_manager.load_offline_token( + validation_result.user_id + ) saas_user_auth = SaasUserAuth( - user_id=user_id, + user_id=validation_result.user_id, refresh_token=SecretStr(offline_token), auth_type=AuthType.BEARER, + api_key_org_id=validation_result.org_id, + api_key_id=validation_result.key_id, + api_key_name=validation_result.key_name, ) await saas_user_auth.refresh() return saas_user_auth diff --git a/enterprise/server/routes/api_keys.py b/enterprise/server/routes/api_keys.py index d5f30f87cf..31320966da 100644 --- a/enterprise/server/routes/api_keys.py +++ b/enterprise/server/routes/api_keys.py @@ -1,7 +1,9 @@ from datetime import UTC, datetime +from typing import cast -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel, field_validator +from server.auth.saas_user_auth import SaasUserAuth from storage.api_key import ApiKey from storage.api_key_store import ApiKeyStore from storage.lite_llm_manager import LiteLlmManager @@ -11,7 +13,8 @@ from storage.org_service import OrgService from storage.user_store import UserStore from openhands.core.logger import openhands_logger as logger -from openhands.server.user_auth import get_user_id +from openhands.server.user_auth import get_user_auth, get_user_id +from openhands.server.user_auth.user_auth import AuthType # Helper functions for BYOR API key management @@ -150,6 +153,16 @@ class MessageResponse(BaseModel): message: str +class CurrentApiKeyResponse(BaseModel): + """Response model for the current API key endpoint.""" + + id: int + name: str | None + org_id: str + user_id: str + auth_type: str + + def api_key_to_response(key: ApiKey) -> ApiKeyResponse: """Convert an ApiKey model to an ApiKeyResponse.""" return ApiKeyResponse( @@ -262,6 +275,46 @@ async def delete_api_key( ) +@api_router.get('/current', tags=['Keys']) +async def get_current_api_key( + request: Request, + user_id: str = Depends(get_user_id), +) -> CurrentApiKeyResponse: + """Get information about the currently authenticated API key. + + This endpoint returns metadata about the API key used for the current request, + including the org_id associated with the key. This is useful for API key + callers who need to know which organization context their key operates in. + + Returns 400 if not authenticated via API key (e.g., using cookie auth). + """ + user_auth = await get_user_auth(request) + + # Check if authenticated via API key + if user_auth.get_auth_type() != AuthType.BEARER: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='This endpoint requires API key authentication. Not available for cookie-based auth.', + ) + + # In SaaS context, bearer auth always produces SaasUserAuth + saas_user_auth = cast(SaasUserAuth, user_auth) + + if saas_user_auth.api_key_org_id is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='This API key was created before organization support. Please regenerate your API key to use this endpoint.', + ) + + return CurrentApiKeyResponse( + id=saas_user_auth.api_key_id, + name=saas_user_auth.api_key_name, + org_id=str(saas_user_auth.api_key_org_id), + user_id=user_id, + auth_type=saas_user_auth.auth_type.value, + ) + + @api_router.get('/llm/byor', tags=['Keys']) async def get_llm_api_key_for_byor( user_id: str = Depends(get_user_id), diff --git a/enterprise/storage/api_key_store.py b/enterprise/storage/api_key_store.py index 74a2d3d73e..3090b8da07 100644 --- a/enterprise/storage/api_key_store.py +++ b/enterprise/storage/api_key_store.py @@ -4,6 +4,7 @@ import secrets import string from dataclasses import dataclass from datetime import UTC, datetime +from uuid import UUID from sqlalchemy import select, update from storage.api_key import ApiKey @@ -13,6 +14,16 @@ from storage.user_store import UserStore from openhands.core.logger import openhands_logger as logger +@dataclass +class ApiKeyValidationResult: + """Result of API key validation containing user and org context.""" + + user_id: str + org_id: UUID | None + key_id: int + key_name: str | None + + @dataclass class ApiKeyStore: API_KEY_PREFIX = 'sk-oh-' @@ -60,8 +71,8 @@ class ApiKeyStore: return api_key - async def validate_api_key(self, api_key: str) -> str | None: - """Validate an API key and return the associated user_id if valid.""" + async def validate_api_key(self, api_key: str) -> ApiKeyValidationResult | None: + """Validate an API key and return the associated user_id and org_id if valid.""" now = datetime.now(UTC) async with a_session_maker() as session: @@ -89,7 +100,12 @@ class ApiKeyStore: ) await session.commit() - return key_record.user_id + return ApiKeyValidationResult( + user_id=key_record.user_id, + org_id=key_record.org_id, + key_id=key_record.id, + key_name=key_record.name, + ) async def delete_api_key(self, api_key: str) -> bool: """Delete an API key by the key value.""" diff --git a/enterprise/storage/saas_conversation_validator.py b/enterprise/storage/saas_conversation_validator.py index bff4468011..51a5302dfc 100644 --- a/enterprise/storage/saas_conversation_validator.py +++ b/enterprise/storage/saas_conversation_validator.py @@ -28,12 +28,14 @@ class SaasConversationValidator(ConversationValidator): # Validate the API key and get the user_id api_key_store = ApiKeyStore.get_instance() - user_id = await api_key_store.validate_api_key(api_key) + validation_result = await api_key_store.validate_api_key(api_key) - if not user_id: + if not validation_result: logger.warning('Invalid API key') return None + user_id = validation_result.user_id + # Get the offline token for the user offline_token = await token_manager.load_offline_token(user_id) if not offline_token: diff --git a/enterprise/tests/unit/server/routes/test_api_keys.py b/enterprise/tests/unit/server/routes/test_api_keys.py index 57a9cb465d..4c35e9d5be 100644 --- a/enterprise/tests/unit/server/routes/test_api_keys.py +++ b/enterprise/tests/unit/server/routes/test_api_keys.py @@ -1,19 +1,26 @@ """Unit tests for API keys routes, focusing on BYOR key validation and retrieval.""" +import uuid from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from fastapi import HTTPException +from pydantic import SecretStr +from server.auth.saas_user_auth import SaasUserAuth from server.routes.api_keys import ( ByorPermittedResponse, + CurrentApiKeyResponse, LlmApiKeyResponse, check_byor_permitted, delete_byor_key_from_litellm, + get_current_api_key, get_llm_api_key_for_byor, ) from storage.lite_llm_manager import LiteLlmManager +from openhands.server.user_auth.user_auth import AuthType + class TestVerifyByorKeyInLitellm: """Test the verify_byor_key_in_litellm function.""" @@ -512,3 +519,81 @@ class TestCheckByorPermitted: assert exc_info.value.status_code == 500 assert 'Failed to check BYOR export permission' in exc_info.value.detail + + +class TestGetCurrentApiKey: + """Test the get_current_api_key endpoint.""" + + @pytest.mark.asyncio + @patch('server.routes.api_keys.get_user_auth') + async def test_returns_api_key_info_for_bearer_auth(self, mock_get_user_auth): + """Test that API key metadata including org_id is returned for bearer token auth.""" + # Arrange + user_id = 'user-123' + org_id = uuid.uuid4() + mock_request = MagicMock() + + user_auth = SaasUserAuth( + refresh_token=SecretStr('mock-token'), + user_id=user_id, + auth_type=AuthType.BEARER, + api_key_org_id=org_id, + api_key_id=42, + api_key_name='My Production Key', + ) + mock_get_user_auth.return_value = user_auth + + # Act + result = await get_current_api_key(request=mock_request, user_id=user_id) + + # Assert + assert isinstance(result, CurrentApiKeyResponse) + assert result.org_id == str(org_id) + assert result.id == 42 + assert result.name == 'My Production Key' + assert result.user_id == user_id + assert result.auth_type == 'bearer' + + @pytest.mark.asyncio + @patch('server.routes.api_keys.get_user_auth') + async def test_returns_400_for_cookie_auth(self, mock_get_user_auth): + """Test that 400 Bad Request is returned when using cookie authentication.""" + # Arrange + user_id = 'user-123' + mock_request = MagicMock() + + mock_user_auth = MagicMock() + mock_user_auth.get_auth_type.return_value = AuthType.COOKIE + mock_get_user_auth.return_value = mock_user_auth + + # Act & Assert + with pytest.raises(HTTPException) as exc_info: + await get_current_api_key(request=mock_request, user_id=user_id) + + assert exc_info.value.status_code == 400 + assert 'API key authentication' in exc_info.value.detail + + @pytest.mark.asyncio + @patch('server.routes.api_keys.get_user_auth') + async def test_returns_400_when_api_key_org_id_is_none(self, mock_get_user_auth): + """Test that 400 is returned when API key has no org_id (legacy key).""" + # Arrange + user_id = 'user-123' + mock_request = MagicMock() + + user_auth = SaasUserAuth( + refresh_token=SecretStr('mock-token'), + user_id=user_id, + auth_type=AuthType.BEARER, + api_key_org_id=None, # No org_id - legacy key + api_key_id=42, + api_key_name='Legacy Key', + ) + mock_get_user_auth.return_value = user_auth + + # Act & Assert + with pytest.raises(HTTPException) as exc_info: + await get_current_api_key(request=mock_request, user_id=user_id) + + assert exc_info.value.status_code == 400 + assert 'created before organization support' in exc_info.value.detail diff --git a/enterprise/tests/unit/test_api_key_store.py b/enterprise/tests/unit/test_api_key_store.py index d3a2d13d1e..baffe5893c 100644 --- a/enterprise/tests/unit/test_api_key_store.py +++ b/enterprise/tests/unit/test_api_key_store.py @@ -126,13 +126,18 @@ async def test_validate_api_key_valid(api_key_store, async_session_maker): ) session.add(key_record) await session.commit() + key_id = key_record.id # Execute - patch a_session_maker to use test's async session maker with patch('storage.api_key_store.a_session_maker', async_session_maker): result = await api_key_store.validate_api_key(api_key_value) - # Verify - assert result == user_id + # Verify - result is now ApiKeyValidationResult + assert result is not None + assert result.user_id == user_id + assert result.org_id == org_id + assert result.key_id == key_id + assert result.key_name == 'Test Key' @pytest.mark.asyncio @@ -218,8 +223,9 @@ async def test_validate_api_key_valid_timezone_naive( with patch('storage.api_key_store.a_session_maker', async_session_maker): result = await api_key_store.validate_api_key(api_key_value) - # Verify - assert result == user_id + # Verify - result is now ApiKeyValidationResult + assert result is not None + assert result.user_id == user_id @pytest.mark.asyncio diff --git a/enterprise/tests/unit/test_saas_user_auth.py b/enterprise/tests/unit/test_saas_user_auth.py index 92552de3ad..2fb1b68445 100644 --- a/enterprise/tests/unit/test_saas_user_auth.py +++ b/enterprise/tests/unit/test_saas_user_auth.py @@ -1,4 +1,5 @@ import time +import uuid from unittest.mock import AsyncMock, MagicMock, patch import jwt @@ -18,6 +19,7 @@ from server.auth.saas_user_auth import ( saas_user_auth_from_cookie, saas_user_auth_from_signed_token, ) +from storage.api_key_store import ApiKeyValidationResult from storage.user_authorization import UserAuthorizationType from openhands.integrations.provider import ProviderToken, ProviderType @@ -468,12 +470,22 @@ async def test_saas_user_auth_from_bearer_success(): algorithm='HS256', ) + mock_org_id = uuid.uuid4() + mock_validation_result = ApiKeyValidationResult( + user_id='test_user_id', + org_id=mock_org_id, + key_id=42, + key_name='Test Key', + ) + with ( patch('server.auth.saas_user_auth.ApiKeyStore') as mock_api_key_store_cls, patch('server.auth.saas_user_auth.token_manager') as mock_token_manager, ): mock_api_key_store = MagicMock() - mock_api_key_store.validate_api_key = AsyncMock(return_value='test_user_id') + mock_api_key_store.validate_api_key = AsyncMock( + return_value=mock_validation_result + ) mock_api_key_store_cls.get_instance.return_value = mock_api_key_store mock_token_manager.load_offline_token = AsyncMock(return_value=offline_token) @@ -485,6 +497,9 @@ async def test_saas_user_auth_from_bearer_success(): assert isinstance(result, SaasUserAuth) assert result.user_id == 'test_user_id' + assert result.api_key_org_id == mock_org_id + assert result.api_key_id == 42 + assert result.api_key_name == 'Test Key' mock_api_key_store.validate_api_key.assert_called_once_with('test_api_key') mock_token_manager.load_offline_token.assert_called_once_with('test_user_id') mock_token_manager.refresh.assert_called_once_with(offline_token) From 1d1ffc2be0454db99cfc77dc05cba5da5e74688c Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Wed, 18 Mar 2026 15:07:36 -0400 Subject: [PATCH 16/21] feat(enterprise): Add service API for automation API key creation (#13467) Co-authored-by: openhands --- enterprise/saas_server.py | 2 + enterprise/server/middleware.py | 4 + enterprise/server/routes/service.py | 270 ++++++++++++++ enterprise/storage/api_key_store.py | 199 ++++++++++- enterprise/tests/unit/routes/__init__.py | 0 enterprise/tests/unit/routes/test_service.py | 331 ++++++++++++++++++ .../tests/unit/storage/test_api_key_store.py | 314 +++++++++++++++++ 7 files changed, 1110 insertions(+), 10 deletions(-) create mode 100644 enterprise/server/routes/service.py create mode 100644 enterprise/tests/unit/routes/__init__.py create mode 100644 enterprise/tests/unit/routes/test_service.py create mode 100644 enterprise/tests/unit/storage/test_api_key_store.py diff --git a/enterprise/saas_server.py b/enterprise/saas_server.py index 8bb576a55b..434652befd 100644 --- a/enterprise/saas_server.py +++ b/enterprise/saas_server.py @@ -46,6 +46,7 @@ from server.routes.org_invitations import ( # noqa: E402 ) from server.routes.orgs import org_router # noqa: E402 from server.routes.readiness import readiness_router # noqa: E402 +from server.routes.service import service_router # noqa: E402 from server.routes.user import saas_user_router # noqa: E402 from server.routes.user_app_settings import user_app_settings_router # noqa: E402 from server.sharing.shared_conversation_router import ( # noqa: E402 @@ -112,6 +113,7 @@ if GITLAB_APP_CLIENT_ID: base_app.include_router(gitlab_integration_router) base_app.include_router(api_keys_router) # Add routes for API key management +base_app.include_router(service_router) # Add routes for internal service API base_app.include_router(org_router) # Add routes for organization management base_app.include_router( verified_models_router diff --git a/enterprise/server/middleware.py b/enterprise/server/middleware.py index 659a66046a..c014864b0b 100644 --- a/enterprise/server/middleware.py +++ b/enterprise/server/middleware.py @@ -182,6 +182,10 @@ class SetAuthCookieMiddleware: if path.startswith('/api/v1/webhooks/'): return False + # Service API uses its own authentication (X-Service-API-Key header) + if path.startswith('/api/service/'): + return False + is_mcp = path.startswith('/mcp') is_api_route = path.startswith('/api') return is_api_route or is_mcp diff --git a/enterprise/server/routes/service.py b/enterprise/server/routes/service.py new file mode 100644 index 0000000000..87e470dd7c --- /dev/null +++ b/enterprise/server/routes/service.py @@ -0,0 +1,270 @@ +""" +Service API routes for internal service-to-service communication. + +This module provides endpoints for trusted internal services (e.g., automations service) +to perform privileged operations like creating API keys on behalf of users. + +Authentication is via a shared secret (X-Service-API-Key header) configured +through the AUTOMATIONS_SERVICE_API_KEY environment variable. +""" + +import os +from uuid import UUID + +from fastapi import APIRouter, Header, HTTPException, status +from pydantic import BaseModel, field_validator +from storage.api_key_store import ApiKeyStore +from storage.org_member_store import OrgMemberStore +from storage.user_store import UserStore + +from openhands.core.logger import openhands_logger as logger + +# Environment variable for the service API key +AUTOMATIONS_SERVICE_API_KEY = os.getenv('AUTOMATIONS_SERVICE_API_KEY', '').strip() + +service_router = APIRouter(prefix='/api/service', tags=['Service']) + + +class CreateUserApiKeyRequest(BaseModel): + """Request model for creating an API key on behalf of a user.""" + + name: str # Required - used to identify the key + + @field_validator('name') + @classmethod + def validate_name(cls, v: str) -> str: + if not v or not v.strip(): + raise ValueError('name is required and cannot be empty') + return v.strip() + + +class CreateUserApiKeyResponse(BaseModel): + """Response model for created API key.""" + + key: str + user_id: str + org_id: str + name: str + + +class ServiceInfoResponse(BaseModel): + """Response model for service info endpoint.""" + + service: str + authenticated: bool + + +async def validate_service_api_key( + x_service_api_key: str | None = Header(default=None, alias='X-Service-API-Key'), +) -> str: + """ + Validate the service API key from the request header. + + Args: + x_service_api_key: The service API key from the X-Service-API-Key header + + Returns: + str: Service identifier for audit logging + + Raises: + HTTPException: 401 if key is missing or invalid + HTTPException: 503 if service auth is not configured + """ + if not AUTOMATIONS_SERVICE_API_KEY: + logger.warning( + 'Service authentication not configured (AUTOMATIONS_SERVICE_API_KEY not set)' + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Service authentication not configured', + ) + + if not x_service_api_key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='X-Service-API-Key header is required', + ) + + if x_service_api_key != AUTOMATIONS_SERVICE_API_KEY: + logger.warning('Invalid service API key attempted') + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid service API key', + ) + + return 'automations-service' + + +@service_router.get('/health') +async def service_health() -> dict: + """Health check endpoint for the service API. + + This endpoint does not require authentication and can be used + to verify the service routes are accessible. + """ + return { + 'status': 'ok', + 'service_auth_configured': bool(AUTOMATIONS_SERVICE_API_KEY), + } + + +@service_router.post('/users/{user_id}/orgs/{org_id}/api-keys') +async def get_or_create_api_key_for_user( + user_id: str, + org_id: UUID, + request: CreateUserApiKeyRequest, + x_service_api_key: str | None = Header(default=None, alias='X-Service-API-Key'), +) -> CreateUserApiKeyResponse: + """ + Get or create an API key for a user on behalf of the automations service. + + If a key with the given name already exists for the user/org and is not expired, + returns the existing key. Otherwise, creates a new key. + + The created/returned keys are system keys and are: + - Not visible to the user in their API keys list + - Not deletable by the user + - Never expire + + Args: + user_id: The user ID + org_id: The organization ID + request: Request body containing name (required) + x_service_api_key: Service API key header for authentication + + Returns: + CreateUserApiKeyResponse: The API key and metadata + + Raises: + HTTPException: 401 if service key is invalid + HTTPException: 404 if user not found + HTTPException: 403 if user is not a member of the specified org + """ + # Validate service API key + service_id = await validate_service_api_key(x_service_api_key) + + # Verify user exists + user = await UserStore.get_user_by_id(user_id) + if not user: + logger.warning( + 'Service attempted to create key for non-existent user', + extra={'user_id': user_id}, + ) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'User {user_id} not found', + ) + + # Verify user is a member of the specified org + org_member = await OrgMemberStore.get_org_member(org_id, UUID(user_id)) + if not org_member: + logger.warning( + 'Service attempted to create key for user not in org', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + }, + ) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f'User {user_id} is not a member of org {org_id}', + ) + + # Get or create the system API key + api_key_store = ApiKeyStore.get_instance() + + try: + api_key = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name=request.name, + ) + except Exception as e: + logger.exception( + 'Failed to get or create system API key', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + 'error': str(e), + }, + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to get or create API key', + ) + + logger.info( + 'Service created API key for user', + extra={ + 'service_id': service_id, + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': request.name, + }, + ) + + return CreateUserApiKeyResponse( + key=api_key, + user_id=user_id, + org_id=str(org_id), + name=request.name, + ) + + +@service_router.delete('/users/{user_id}/orgs/{org_id}/api-keys/{key_name}') +async def delete_user_api_key( + user_id: str, + org_id: UUID, + key_name: str, + x_service_api_key: str | None = Header(default=None, alias='X-Service-API-Key'), +) -> dict: + """ + Delete a system API key created by the service. + + This endpoint allows the automations service to clean up API keys + it previously created for users. + + Args: + user_id: The user ID + org_id: The organization ID + key_name: The name of the key to delete (without __SYSTEM__: prefix) + x_service_api_key: Service API key header for authentication + + Returns: + dict: Success message + + Raises: + HTTPException: 401 if service key is invalid + HTTPException: 404 if key not found + """ + # Validate service API key + service_id = await validate_service_api_key(x_service_api_key) + + api_key_store = ApiKeyStore.get_instance() + + # Delete the key by name (wrap with system key prefix since service creates system keys) + system_key_name = api_key_store.make_system_key_name(key_name) + success = await api_key_store.delete_api_key_by_name( + user_id=user_id, + org_id=org_id, + name=system_key_name, + allow_system=True, + ) + + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'API key with name "{key_name}" not found for user {user_id} in org {org_id}', + ) + + logger.info( + 'Service deleted API key for user', + extra={ + 'service_id': service_id, + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': key_name, + }, + ) + + return {'message': 'API key deleted successfully'} diff --git a/enterprise/storage/api_key_store.py b/enterprise/storage/api_key_store.py index 3090b8da07..ecbb375592 100644 --- a/enterprise/storage/api_key_store.py +++ b/enterprise/storage/api_key_store.py @@ -27,6 +27,9 @@ class ApiKeyValidationResult: @dataclass class ApiKeyStore: API_KEY_PREFIX = 'sk-oh-' + # Prefix for system keys created by internal services (e.g., automations) + # Keys with this prefix are hidden from users and cannot be deleted by users + SYSTEM_KEY_NAME_PREFIX = '__SYSTEM__:' def generate_api_key(self, length: int = 32) -> str: """Generate a random API key with the sk-oh- prefix.""" @@ -34,6 +37,19 @@ class ApiKeyStore: random_part = ''.join(secrets.choice(alphabet) for _ in range(length)) return f'{self.API_KEY_PREFIX}{random_part}' + @classmethod + def is_system_key_name(cls, name: str | None) -> bool: + """Check if a key name indicates a system key.""" + return name is not None and name.startswith(cls.SYSTEM_KEY_NAME_PREFIX) + + @classmethod + def make_system_key_name(cls, name: str) -> str: + """Create a system key name with the appropriate prefix. + + Format: __SYSTEM__: + """ + return f'{cls.SYSTEM_KEY_NAME_PREFIX}{name}' + async def create_api_key( self, user_id: str, name: str | None = None, expires_at: datetime | None = None ) -> str: @@ -71,6 +87,113 @@ class ApiKeyStore: return api_key + async def get_or_create_system_api_key( + self, + user_id: str, + org_id: UUID, + name: str, + ) -> str: + """Get or create a system API key for a user on behalf of an internal service. + + If a key with the given name already exists for this user/org and is not expired, + returns the existing key. Otherwise, creates a new key (and deletes any expired one). + + System keys are: + - Not visible to users in their API keys list (filtered by name prefix) + - Not deletable by users (protected by name prefix check) + - Associated with a specific org (not the user's current org) + - Never expire (no expiration date) + + Args: + user_id: The ID of the user to create the key for + org_id: The organization ID to associate the key with + name: Required name for the key (will be prefixed with __SYSTEM__:) + + Returns: + The API key (existing or newly created) + """ + # Create system key name with prefix + system_key_name = self.make_system_key_name(name) + + async with a_session_maker() as session: + # Check if key already exists for this user/org/name + result = await session.execute( + select(ApiKey).filter( + ApiKey.user_id == user_id, + ApiKey.org_id == org_id, + ApiKey.name == system_key_name, + ) + ) + existing_key = result.scalars().first() + + if existing_key: + # Check if expired + if existing_key.expires_at: + now = datetime.now(UTC) + expires_at = existing_key.expires_at + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=UTC) + + if expires_at < now: + # Key is expired, delete it and create new one + logger.info( + 'System API key expired, re-issuing', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': system_key_name, + }, + ) + await session.delete(existing_key) + await session.commit() + else: + # Key exists and is not expired, return it + logger.debug( + 'Returning existing system API key', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': system_key_name, + }, + ) + return existing_key.key + else: + # Key exists and has no expiration, return it + logger.debug( + 'Returning existing system API key', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': system_key_name, + }, + ) + return existing_key.key + + # Create new key (no expiration) + api_key = self.generate_api_key() + + async with a_session_maker() as session: + key_record = ApiKey( + key=api_key, + user_id=user_id, + org_id=org_id, + name=system_key_name, + expires_at=None, # System keys never expire + ) + session.add(key_record) + await session.commit() + + logger.info( + 'Created system API key', + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + 'key_name': system_key_name, + }, + ) + + return api_key + async def validate_api_key(self, api_key: str) -> ApiKeyValidationResult | None: """Validate an API key and return the associated user_id and org_id if valid.""" now = datetime.now(UTC) @@ -121,8 +244,18 @@ class ApiKeyStore: return True - async def delete_api_key_by_id(self, key_id: int) -> bool: - """Delete an API key by its ID.""" + async def delete_api_key_by_id( + self, key_id: int, allow_system: bool = False + ) -> bool: + """Delete an API key by its ID. + + Args: + key_id: The ID of the key to delete + allow_system: If False (default), system keys cannot be deleted + + Returns: + True if the key was deleted, False if not found or is a protected system key + """ async with a_session_maker() as session: result = await session.execute(select(ApiKey).filter(ApiKey.id == key_id)) key_record = result.scalars().first() @@ -130,13 +263,26 @@ class ApiKeyStore: if not key_record: return False + # Protect system keys from deletion unless explicitly allowed + if self.is_system_key_name(key_record.name) and not allow_system: + logger.warning( + 'Attempted to delete system API key', + extra={'key_id': key_id, 'user_id': key_record.user_id}, + ) + return False + await session.delete(key_record) await session.commit() return True async def list_api_keys(self, user_id: str) -> list[ApiKey]: - """List all API keys for a user.""" + """List all user-visible API keys for a user. + + This excludes: + - System keys (name starts with __SYSTEM__:) - created by internal services + - MCP_API_KEY - internal MCP key + """ user = await UserStore.get_user_by_id(user_id) if user is None: raise ValueError(f'User not found: {user_id}') @@ -145,11 +291,17 @@ class ApiKeyStore: async with a_session_maker() as session: result = await session.execute( select(ApiKey).filter( - ApiKey.user_id == user_id, ApiKey.org_id == org_id + ApiKey.user_id == user_id, + ApiKey.org_id == org_id, ) ) keys = result.scalars().all() - return [key for key in keys if key.name != 'MCP_API_KEY'] + # Filter out system keys and MCP_API_KEY + return [ + key + for key in keys + if key.name != 'MCP_API_KEY' and not self.is_system_key_name(key.name) + ] async def retrieve_mcp_api_key(self, user_id: str) -> str | None: user = await UserStore.get_user_by_id(user_id) @@ -179,17 +331,44 @@ class ApiKeyStore: key_record = result.scalars().first() return key_record.key if key_record else None - async def delete_api_key_by_name(self, user_id: str, name: str) -> bool: - """Delete an API key by name for a specific user.""" + async def delete_api_key_by_name( + self, + user_id: str, + name: str, + org_id: UUID | None = None, + allow_system: bool = False, + ) -> bool: + """Delete an API key by name for a specific user. + + Args: + user_id: The ID of the user whose key to delete + name: The name of the key to delete + org_id: Optional organization ID to filter by (required for system keys) + allow_system: If False (default), system keys cannot be deleted + + Returns: + True if the key was deleted, False if not found or is a protected system key + """ async with a_session_maker() as session: - result = await session.execute( - select(ApiKey).filter(ApiKey.user_id == user_id, ApiKey.name == name) - ) + # Build the query filters + filters = [ApiKey.user_id == user_id, ApiKey.name == name] + if org_id is not None: + filters.append(ApiKey.org_id == org_id) + + result = await session.execute(select(ApiKey).filter(*filters)) key_record = result.scalars().first() if not key_record: return False + # Protect system keys from deletion unless explicitly allowed + if self.is_system_key_name(key_record.name) and not allow_system: + logger.warning( + 'Attempted to delete system API key', + extra={'user_id': user_id, 'key_name': name}, + ) + return False + await session.delete(key_record) await session.commit() diff --git a/enterprise/tests/unit/routes/__init__.py b/enterprise/tests/unit/routes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/enterprise/tests/unit/routes/test_service.py b/enterprise/tests/unit/routes/test_service.py new file mode 100644 index 0000000000..a7156ec117 --- /dev/null +++ b/enterprise/tests/unit/routes/test_service.py @@ -0,0 +1,331 @@ +"""Unit tests for service API routes.""" + +import uuid +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fastapi import HTTPException +from server.routes.service import ( + CreateUserApiKeyRequest, + delete_user_api_key, + get_or_create_api_key_for_user, + validate_service_api_key, +) + + +class TestValidateServiceApiKey: + """Test cases for validate_service_api_key.""" + + @pytest.mark.asyncio + async def test_valid_service_key(self): + """Test validation with valid service API key.""" + with patch( + 'server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-service-key' + ): + result = await validate_service_api_key('test-service-key') + assert result == 'automations-service' + + @pytest.mark.asyncio + async def test_missing_service_key(self): + """Test validation with missing service API key header.""" + with patch( + 'server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-service-key' + ): + with pytest.raises(HTTPException) as exc_info: + await validate_service_api_key(None) + assert exc_info.value.status_code == 401 + assert 'X-Service-API-Key header is required' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_invalid_service_key(self): + """Test validation with invalid service API key.""" + with patch( + 'server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-service-key' + ): + with pytest.raises(HTTPException) as exc_info: + await validate_service_api_key('wrong-key') + assert exc_info.value.status_code == 401 + assert 'Invalid service API key' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_service_auth_not_configured(self): + """Test validation when service auth is not configured.""" + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', ''): + with pytest.raises(HTTPException) as exc_info: + await validate_service_api_key('any-key') + assert exc_info.value.status_code == 503 + assert 'Service authentication not configured' in exc_info.value.detail + + +class TestCreateUserApiKeyRequest: + """Test cases for CreateUserApiKeyRequest validation.""" + + def test_valid_request(self): + """Test valid request with all fields.""" + request = CreateUserApiKeyRequest( + name='automation', + ) + assert request.name == 'automation' + + def test_name_is_required(self): + """Test that name field is required.""" + with pytest.raises(ValueError): + CreateUserApiKeyRequest( + name='', # Empty name should fail + ) + + def test_name_is_stripped(self): + """Test that name field is stripped of whitespace.""" + request = CreateUserApiKeyRequest( + name=' automation ', + ) + assert request.name == 'automation' + + def test_whitespace_only_name_fails(self): + """Test that whitespace-only name fails validation.""" + with pytest.raises(ValueError): + CreateUserApiKeyRequest( + name=' ', + ) + + +class TestGetOrCreateApiKeyForUser: + """Test cases for get_or_create_api_key_for_user endpoint.""" + + @pytest.fixture + def valid_user_id(self): + """Return a valid user ID.""" + return '5594c7b6-f959-4b81-92e9-b09c206f5081' + + @pytest.fixture + def valid_org_id(self): + """Return a valid org ID.""" + return uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + @pytest.fixture + def valid_request(self): + """Create a valid request object.""" + return CreateUserApiKeyRequest( + name='automation', + ) + + @pytest.mark.asyncio + async def test_user_not_found(self, valid_user_id, valid_org_id, valid_request): + """Test error when user doesn't exist.""" + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.UserStore.get_user_by_id', new_callable=AsyncMock + ) as mock_get_user: + mock_get_user.return_value = None + with pytest.raises(HTTPException) as exc_info: + await get_or_create_api_key_for_user( + user_id=valid_user_id, + org_id=valid_org_id, + request=valid_request, + x_service_api_key='test-key', + ) + assert exc_info.value.status_code == 404 + assert 'not found' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_user_not_in_org(self, valid_user_id, valid_org_id, valid_request): + """Test error when user is not a member of the org.""" + mock_user = MagicMock() + + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.UserStore.get_user_by_id', new_callable=AsyncMock + ) as mock_get_user: + with patch( + 'server.routes.service.OrgMemberStore.get_org_member', + new_callable=AsyncMock, + ) as mock_get_member: + mock_get_user.return_value = mock_user + mock_get_member.return_value = None + with pytest.raises(HTTPException) as exc_info: + await get_or_create_api_key_for_user( + user_id=valid_user_id, + org_id=valid_org_id, + request=valid_request, + x_service_api_key='test-key', + ) + assert exc_info.value.status_code == 403 + assert 'not a member of org' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_successful_key_creation( + self, valid_user_id, valid_org_id, valid_request + ): + """Test successful API key creation.""" + mock_user = MagicMock() + mock_org_member = MagicMock() + mock_api_key_store = MagicMock() + mock_api_key_store.get_or_create_system_api_key = AsyncMock( + return_value='sk-oh-test-key-12345678901234567890' + ) + + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.UserStore.get_user_by_id', new_callable=AsyncMock + ) as mock_get_user: + with patch( + 'server.routes.service.OrgMemberStore.get_org_member', + new_callable=AsyncMock, + ) as mock_get_member: + with patch( + 'server.routes.service.ApiKeyStore.get_instance' + ) as mock_get_store: + mock_get_user.return_value = mock_user + mock_get_member.return_value = mock_org_member + mock_get_store.return_value = mock_api_key_store + + response = await get_or_create_api_key_for_user( + user_id=valid_user_id, + org_id=valid_org_id, + request=valid_request, + x_service_api_key='test-key', + ) + + assert response.key == 'sk-oh-test-key-12345678901234567890' + assert response.user_id == valid_user_id + assert response.org_id == str(valid_org_id) + assert response.name == 'automation' + + # Verify the store was called with correct arguments + mock_api_key_store.get_or_create_system_api_key.assert_called_once_with( + user_id=valid_user_id, + org_id=valid_org_id, + name='automation', + ) + + @pytest.mark.asyncio + async def test_store_exception_handling( + self, valid_user_id, valid_org_id, valid_request + ): + """Test error handling when store raises exception.""" + mock_user = MagicMock() + mock_org_member = MagicMock() + mock_api_key_store = MagicMock() + mock_api_key_store.get_or_create_system_api_key = AsyncMock( + side_effect=Exception('Database error') + ) + + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.UserStore.get_user_by_id', new_callable=AsyncMock + ) as mock_get_user: + with patch( + 'server.routes.service.OrgMemberStore.get_org_member', + new_callable=AsyncMock, + ) as mock_get_member: + with patch( + 'server.routes.service.ApiKeyStore.get_instance' + ) as mock_get_store: + mock_get_user.return_value = mock_user + mock_get_member.return_value = mock_org_member + mock_get_store.return_value = mock_api_key_store + + with pytest.raises(HTTPException) as exc_info: + await get_or_create_api_key_for_user( + user_id=valid_user_id, + org_id=valid_org_id, + request=valid_request, + x_service_api_key='test-key', + ) + + assert exc_info.value.status_code == 500 + assert 'Failed to get or create API key' in exc_info.value.detail + + +class TestDeleteUserApiKey: + """Test cases for delete_user_api_key endpoint.""" + + @pytest.fixture + def valid_org_id(self): + """Return a valid org ID.""" + return uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + @pytest.mark.asyncio + async def test_successful_delete(self, valid_org_id): + """Test successful deletion of a system API key.""" + mock_api_key_store = MagicMock() + mock_api_key_store.make_system_key_name.return_value = '__SYSTEM__:automation' + mock_api_key_store.delete_api_key_by_name = AsyncMock(return_value=True) + + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.ApiKeyStore.get_instance' + ) as mock_get_store: + mock_get_store.return_value = mock_api_key_store + + response = await delete_user_api_key( + user_id='user-123', + org_id=valid_org_id, + key_name='automation', + x_service_api_key='test-key', + ) + + assert response == {'message': 'API key deleted successfully'} + + # Verify the store was called with correct arguments + mock_api_key_store.make_system_key_name.assert_called_once_with('automation') + mock_api_key_store.delete_api_key_by_name.assert_called_once_with( + user_id='user-123', + org_id=valid_org_id, + name='__SYSTEM__:automation', + allow_system=True, + ) + + @pytest.mark.asyncio + async def test_delete_key_not_found(self, valid_org_id): + """Test error when key to delete is not found.""" + mock_api_key_store = MagicMock() + mock_api_key_store.make_system_key_name.return_value = '__SYSTEM__:nonexistent' + mock_api_key_store.delete_api_key_by_name = AsyncMock(return_value=False) + + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with patch( + 'server.routes.service.ApiKeyStore.get_instance' + ) as mock_get_store: + mock_get_store.return_value = mock_api_key_store + + with pytest.raises(HTTPException) as exc_info: + await delete_user_api_key( + user_id='user-123', + org_id=valid_org_id, + key_name='nonexistent', + x_service_api_key='test-key', + ) + + assert exc_info.value.status_code == 404 + assert 'not found' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_delete_invalid_service_key(self, valid_org_id): + """Test error when service API key is invalid.""" + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with pytest.raises(HTTPException) as exc_info: + await delete_user_api_key( + user_id='user-123', + org_id=valid_org_id, + key_name='automation', + x_service_api_key='wrong-key', + ) + + assert exc_info.value.status_code == 401 + assert 'Invalid service API key' in exc_info.value.detail + + @pytest.mark.asyncio + async def test_delete_missing_service_key(self, valid_org_id): + """Test error when service API key header is missing.""" + with patch('server.routes.service.AUTOMATIONS_SERVICE_API_KEY', 'test-key'): + with pytest.raises(HTTPException) as exc_info: + await delete_user_api_key( + user_id='user-123', + org_id=valid_org_id, + key_name='automation', + x_service_api_key=None, + ) + + assert exc_info.value.status_code == 401 + assert 'X-Service-API-Key header is required' in exc_info.value.detail diff --git a/enterprise/tests/unit/storage/test_api_key_store.py b/enterprise/tests/unit/storage/test_api_key_store.py new file mode 100644 index 0000000000..0db2d8bb96 --- /dev/null +++ b/enterprise/tests/unit/storage/test_api_key_store.py @@ -0,0 +1,314 @@ +"""Unit tests for ApiKeyStore system key functionality.""" + +import uuid +from datetime import UTC, datetime, timedelta +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from sqlalchemy import select +from storage.api_key import ApiKey +from storage.api_key_store import ApiKeyStore + + +@pytest.fixture +def api_key_store(): + """Create ApiKeyStore instance.""" + return ApiKeyStore() + + +class TestApiKeyStoreSystemKeys: + """Test cases for system API key functionality.""" + + def test_is_system_key_name_with_prefix(self, api_key_store): + """Test that names with __SYSTEM__: prefix are identified as system keys.""" + assert api_key_store.is_system_key_name('__SYSTEM__:automation') is True + assert api_key_store.is_system_key_name('__SYSTEM__:test-key') is True + assert api_key_store.is_system_key_name('__SYSTEM__:') is True + + def test_is_system_key_name_without_prefix(self, api_key_store): + """Test that names without __SYSTEM__: prefix are not system keys.""" + assert api_key_store.is_system_key_name('my-key') is False + assert api_key_store.is_system_key_name('automation') is False + assert api_key_store.is_system_key_name('MCP_API_KEY') is False + assert api_key_store.is_system_key_name('') is False + + def test_is_system_key_name_none(self, api_key_store): + """Test that None is not a system key.""" + assert api_key_store.is_system_key_name(None) is False + + def test_make_system_key_name(self, api_key_store): + """Test system key name generation.""" + assert ( + api_key_store.make_system_key_name('automation') == '__SYSTEM__:automation' + ) + assert api_key_store.make_system_key_name('test-key') == '__SYSTEM__:test-key' + + @pytest.mark.asyncio + async def test_get_or_create_system_api_key_creates_new( + self, api_key_store, async_session_maker + ): + """Test creating a new system API key when none exists.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + key_name = 'automation' + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + api_key = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name=key_name, + ) + + assert api_key.startswith('sk-oh-') + assert len(api_key) == len('sk-oh-') + 32 + + # Verify the key was created in the database + async with async_session_maker() as session: + result = await session.execute(select(ApiKey).filter(ApiKey.key == api_key)) + key_record = result.scalars().first() + assert key_record is not None + assert key_record.user_id == user_id + assert key_record.org_id == org_id + assert key_record.name == '__SYSTEM__:automation' + assert key_record.expires_at is None # System keys never expire + + @pytest.mark.asyncio + async def test_get_or_create_system_api_key_returns_existing( + self, api_key_store, async_session_maker + ): + """Test that existing valid system key is returned.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + key_name = 'automation' + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + # Create the first key + first_key = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name=key_name, + ) + + # Request again - should return the same key + second_key = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name=key_name, + ) + + assert first_key == second_key + + @pytest.mark.asyncio + async def test_get_or_create_system_api_key_different_names( + self, api_key_store, async_session_maker + ): + """Test that different names create different keys.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + key1 = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name='automation-1', + ) + + key2 = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name='automation-2', + ) + + assert key1 != key2 + + @pytest.mark.asyncio + async def test_get_or_create_system_api_key_reissues_expired( + self, api_key_store, async_session_maker + ): + """Test that expired system key is replaced with a new one.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + key_name = 'automation' + system_key_name = '__SYSTEM__:automation' + + # First, manually create an expired key + expired_time = datetime.now(UTC) - timedelta(hours=1) + async with async_session_maker() as session: + expired_key = ApiKey( + key='sk-oh-expired-key-12345678901234567890', + user_id=user_id, + org_id=org_id, + name=system_key_name, + expires_at=expired_time.replace(tzinfo=None), + ) + session.add(expired_key) + await session.commit() + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + # Request the key - should create a new one + new_key = await api_key_store.get_or_create_system_api_key( + user_id=user_id, + org_id=org_id, + name=key_name, + ) + + assert new_key != 'sk-oh-expired-key-12345678901234567890' + assert new_key.startswith('sk-oh-') + + # Verify old key was deleted and new key exists + async with async_session_maker() as session: + result = await session.execute( + select(ApiKey).filter(ApiKey.name == system_key_name) + ) + keys = result.scalars().all() + assert len(keys) == 1 + assert keys[0].key == new_key + assert keys[0].expires_at is None + + @pytest.mark.asyncio + async def test_list_api_keys_excludes_system_keys( + self, api_key_store, async_session_maker + ): + """Test that list_api_keys excludes system keys.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + # Create a user key and a system key + async with async_session_maker() as session: + user_key = ApiKey( + key='sk-oh-user-key-123456789012345678901', + user_id=user_id, + org_id=org_id, + name='my-user-key', + ) + system_key = ApiKey( + key='sk-oh-system-key-12345678901234567890', + user_id=user_id, + org_id=org_id, + name='__SYSTEM__:automation', + ) + mcp_key = ApiKey( + key='sk-oh-mcp-key-1234567890123456789012', + user_id=user_id, + org_id=org_id, + name='MCP_API_KEY', + ) + session.add(user_key) + session.add(system_key) + session.add(mcp_key) + await session.commit() + + # Mock UserStore.get_user_by_id to return a user with the correct org + mock_user = MagicMock() + mock_user.current_org_id = org_id + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + with patch( + 'storage.api_key_store.UserStore.get_user_by_id', new_callable=AsyncMock + ) as mock_get_user: + mock_get_user.return_value = mock_user + keys = await api_key_store.list_api_keys(user_id) + + # Should only return the user key + assert len(keys) == 1 + assert keys[0].name == 'my-user-key' + + @pytest.mark.asyncio + async def test_delete_api_key_by_id_protects_system_keys( + self, api_key_store, async_session_maker + ): + """Test that system keys cannot be deleted by users.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + # Create a system key + async with async_session_maker() as session: + system_key = ApiKey( + key='sk-oh-system-key-12345678901234567890', + user_id=user_id, + org_id=org_id, + name='__SYSTEM__:automation', + ) + session.add(system_key) + await session.commit() + key_id = system_key.id + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + # Attempt to delete without allow_system flag + result = await api_key_store.delete_api_key_by_id( + key_id, allow_system=False + ) + + assert result is False + + # Verify the key still exists + async with async_session_maker() as session: + result = await session.execute(select(ApiKey).filter(ApiKey.id == key_id)) + key_record = result.scalars().first() + assert key_record is not None + + @pytest.mark.asyncio + async def test_delete_api_key_by_id_allows_system_with_flag( + self, api_key_store, async_session_maker + ): + """Test that system keys can be deleted with allow_system=True.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + # Create a system key + async with async_session_maker() as session: + system_key = ApiKey( + key='sk-oh-system-key-12345678901234567890', + user_id=user_id, + org_id=org_id, + name='__SYSTEM__:automation', + ) + session.add(system_key) + await session.commit() + key_id = system_key.id + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + # Delete with allow_system=True + result = await api_key_store.delete_api_key_by_id(key_id, allow_system=True) + + assert result is True + + # Verify the key was deleted + async with async_session_maker() as session: + result = await session.execute(select(ApiKey).filter(ApiKey.id == key_id)) + key_record = result.scalars().first() + assert key_record is None + + @pytest.mark.asyncio + async def test_delete_api_key_by_id_allows_regular_keys( + self, api_key_store, async_session_maker + ): + """Test that regular keys can be deleted normally.""" + user_id = '5594c7b6-f959-4b81-92e9-b09c206f5081' + org_id = uuid.UUID('5594c7b6-f959-4b81-92e9-b09c206f5081') + + # Create a regular key + async with async_session_maker() as session: + regular_key = ApiKey( + key='sk-oh-regular-key-1234567890123456789', + user_id=user_id, + org_id=org_id, + name='my-regular-key', + ) + session.add(regular_key) + await session.commit() + key_id = regular_key.id + + with patch('storage.api_key_store.a_session_maker', async_session_maker): + # Delete without allow_system flag - should work for regular keys + result = await api_key_store.delete_api_key_by_id( + key_id, allow_system=False + ) + + assert result is True + + # Verify the key was deleted + async with async_session_maker() as session: + result = await session.execute(select(ApiKey).filter(ApiKey.id == key_id)) + key_record = result.scalars().first() + assert key_record is None From 2879e587813b8a128c155d9e2bbcece862b6153c Mon Sep 17 00:00:00 2001 From: aivong-openhands Date: Wed, 18 Mar 2026 15:00:06 -0500 Subject: [PATCH 17/21] Fix CVE-2026-30922: Update pyasn1 to 0.6.3 (#13452) Co-authored-by: OpenHands CVE Fix Bot --- enterprise/poetry.lock | 6 +++--- poetry.lock | 6 +++--- uv.lock | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/enterprise/poetry.lock b/enterprise/poetry.lock index 589be34bb0..1bb48f24c6 100644 --- a/enterprise/poetry.lock +++ b/enterprise/poetry.lock @@ -7597,14 +7597,14 @@ wrappers-encryption = ["cryptography (>=45.0.0)"] [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"}, - {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"}, + {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, + {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, ] [[package]] diff --git a/poetry.lock b/poetry.lock index 5b0b30f61d..bccd0eea80 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7589,14 +7589,14 @@ wrappers-encryption = ["cryptography (>=45.0.0)"] [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"}, - {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"}, + {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, + {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, ] [[package]] diff --git a/uv.lock b/uv.lock index aec35e87db..67c7965698 100644 --- a/uv.lock +++ b/uv.lock @@ -4643,11 +4643,11 @@ memory = [ [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] [[package]] From abd1f9948f888c73440b94334e57771aabee5556 Mon Sep 17 00:00:00 2001 From: HeyItsChloe <54480367+HeyItsChloe@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:46:00 -0700 Subject: [PATCH 18/21] fix: return empty skills list instead of 404 for stopped sandboxes (#13429) Co-authored-by: openhands --- .../app_conversation_router.py | 26 ++++++++--- .../test_app_conversation_hooks_endpoint.py | 43 ++++++++++++++++++- .../test_app_conversation_skills_endpoint.py | 21 +++------ 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/openhands/app_server/app_conversation/app_conversation_router.py b/openhands/app_server/app_conversation/app_conversation_router.py index d3ad901db7..582de93761 100644 --- a/openhands/app_server/app_conversation/app_conversation_router.py +++ b/openhands/app_server/app_conversation/app_conversation_router.py @@ -115,7 +115,7 @@ async def _get_agent_server_context( app_conversation_service: AppConversationService, sandbox_service: SandboxService, sandbox_spec_service: SandboxSpecService, -) -> AgentServerContext | JSONResponse: +) -> AgentServerContext | JSONResponse | None: """Get the agent server context for a conversation. This helper retrieves all necessary information to communicate with the @@ -129,7 +129,8 @@ async def _get_agent_server_context( sandbox_spec_service: Service for sandbox spec operations Returns: - AgentServerContext if successful, or JSONResponse with error details. + AgentServerContext if successful, JSONResponse(404) if conversation + not found, or None if sandbox is not running (e.g. closed conversation). """ # Get the conversation info conversation = await app_conversation_service.get_app_conversation(conversation_id) @@ -141,12 +142,19 @@ async def _get_agent_server_context( # Get the sandbox info sandbox = await sandbox_service.get_sandbox(conversation.sandbox_id) - if not sandbox or sandbox.status != SandboxStatus.RUNNING: + if not sandbox: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, - content={ - 'error': f'Sandbox not found or not running for conversation {conversation_id}' - }, + content={'error': f'Sandbox not found for conversation {conversation_id}'}, + ) + # Return None for paused sandboxes (closed conversation) + if sandbox.status == SandboxStatus.PAUSED: + return None + # Return 404 for other non-running states (STARTING, ERROR, MISSING) + if sandbox.status != SandboxStatus.RUNNING: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={'error': f'Sandbox not ready for conversation {conversation_id}'}, ) # Get the sandbox spec to find the working directory @@ -587,6 +595,7 @@ async def get_conversation_skills( Returns: JSONResponse: A JSON response containing the list of skills. + Returns an empty list if the sandbox is not running. """ try: # Get agent server context (conversation, sandbox, sandbox_spec, agent_server_url) @@ -598,6 +607,8 @@ async def get_conversation_skills( ) if isinstance(ctx, JSONResponse): return ctx + if ctx is None: + return JSONResponse(status_code=status.HTTP_200_OK, content={'skills': []}) # Load skills from all sources logger.info(f'Loading skills for conversation {conversation_id}') @@ -685,6 +696,7 @@ async def get_conversation_hooks( Returns: JSONResponse: A JSON response containing the list of hook event types. + Returns an empty list if the sandbox is not running. """ try: # Get agent server context (conversation, sandbox, sandbox_spec, agent_server_url) @@ -696,6 +708,8 @@ async def get_conversation_hooks( ) if isinstance(ctx, JSONResponse): return ctx + if ctx is None: + return JSONResponse(status_code=status.HTTP_200_OK, content={'hooks': []}) from openhands.app_server.app_conversation.hook_loader import ( fetch_hooks_from_agent_server, diff --git a/tests/unit/app_server/test_app_conversation_hooks_endpoint.py b/tests/unit/app_server/test_app_conversation_hooks_endpoint.py index ba67c4b488..ffc8c54d37 100644 --- a/tests/unit/app_server/test_app_conversation_hooks_endpoint.py +++ b/tests/unit/app_server/test_app_conversation_hooks_endpoint.py @@ -263,7 +263,7 @@ class TestGetConversationHooks: assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_get_hooks_returns_404_when_sandbox_not_running(self): + async def test_get_hooks_returns_404_when_sandbox_not_found(self): conversation_id = uuid4() sandbox_id = str(uuid4()) @@ -291,3 +291,44 @@ class TestGetConversationHooks: ) assert response.status_code == status.HTTP_404_NOT_FOUND + + async def test_get_hooks_returns_empty_list_when_sandbox_paused(self): + conversation_id = uuid4() + sandbox_id = str(uuid4()) + + mock_conversation = AppConversation( + id=conversation_id, + created_by_user_id='test-user', + sandbox_id=sandbox_id, + sandbox_status=SandboxStatus.PAUSED, + ) + + mock_sandbox = SandboxInfo( + id=sandbox_id, + created_by_user_id='test-user', + status=SandboxStatus.PAUSED, + sandbox_spec_id=str(uuid4()), + session_api_key='test-api-key', + ) + + mock_app_conversation_service = MagicMock() + mock_app_conversation_service.get_app_conversation = AsyncMock( + return_value=mock_conversation + ) + + mock_sandbox_service = MagicMock() + mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox) + + response = await get_conversation_hooks( + conversation_id=conversation_id, + app_conversation_service=mock_app_conversation_service, + sandbox_service=mock_sandbox_service, + sandbox_spec_service=MagicMock(), + httpx_client=AsyncMock(spec=httpx.AsyncClient), + ) + + assert response.status_code == status.HTTP_200_OK + import json + + data = json.loads(response.body.decode('utf-8')) + assert data == {'hooks': []} diff --git a/tests/unit/app_server/test_app_conversation_skills_endpoint.py b/tests/unit/app_server/test_app_conversation_skills_endpoint.py index ed7fedd43d..6b601cf9db 100644 --- a/tests/unit/app_server/test_app_conversation_skills_endpoint.py +++ b/tests/unit/app_server/test_app_conversation_skills_endpoint.py @@ -203,7 +203,7 @@ class TestGetConversationSkills: Arrange: Setup conversation but no sandbox Act: Call get_conversation_skills endpoint - Assert: Response is 404 with sandbox error message + Assert: Response is 404 """ # Arrange conversation_id = uuid4() @@ -237,19 +237,13 @@ class TestGetConversationSkills: # Assert assert response.status_code == status.HTTP_404_NOT_FOUND - content = response.body.decode('utf-8') - import json - data = json.loads(content) - assert 'error' in data - assert 'Sandbox not found' in data['error'] + async def test_get_skills_returns_empty_list_when_sandbox_paused(self): + """Test endpoint returns empty skills when sandbox is PAUSED (closed conversation). - async def test_get_skills_returns_404_when_sandbox_not_running(self): - """Test endpoint returns 404 when sandbox is not in RUNNING state. - - Arrange: Setup conversation with stopped sandbox + Arrange: Setup conversation with paused sandbox Act: Call get_conversation_skills endpoint - Assert: Response is 404 with sandbox not running message + Assert: Response is 200 with empty skills list """ # Arrange conversation_id = uuid4() @@ -290,13 +284,12 @@ class TestGetConversationSkills: ) # Assert - assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.status_code == status.HTTP_200_OK content = response.body.decode('utf-8') import json data = json.loads(content) - assert 'error' in data - assert 'not running' in data['error'] + assert data == {'skills': []} async def test_get_skills_handles_task_trigger_skills(self): """Test endpoint correctly handles skills with TaskTrigger. From 7edebcbc0c09fd0edc6c666b327919f267e12197 Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Wed, 18 Mar 2026 16:49:32 -0600 Subject: [PATCH 19/21] fix: use atomic write in LocalFileStore to prevent race conditions (#13480) Co-authored-by: openhands Co-authored-by: OpenHands Bot --- openhands/storage/local.py | 17 ++++++++-- tests/unit/storage/test_storage.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/openhands/storage/local.py b/openhands/storage/local.py index fcb766c0ef..e646f3137e 100644 --- a/openhands/storage/local.py +++ b/openhands/storage/local.py @@ -1,5 +1,6 @@ import os import shutil +import threading from openhands.core.logger import openhands_logger as logger from openhands.storage.files import FileStore @@ -23,8 +24,20 @@ class LocalFileStore(FileStore): full_path = self.get_full_path(path) os.makedirs(os.path.dirname(full_path), exist_ok=True) mode = 'w' if isinstance(contents, str) else 'wb' - with open(full_path, mode) as f: - f.write(contents) + + # Use atomic write: write to temp file, then rename + # This prevents race conditions where concurrent writes could corrupt the file + temp_path = f'{full_path}.tmp.{os.getpid()}.{threading.get_ident()}' + try: + with open(temp_path, mode) as f: + f.write(contents) + f.flush() + os.fsync(f.fileno()) + os.replace(temp_path, full_path) + except Exception: + if os.path.exists(temp_path): + os.remove(temp_path) + raise def read(self, path: str) -> str: full_path = self.get_full_path(path) diff --git a/tests/unit/storage/test_storage.py b/tests/unit/storage/test_storage.py index a78c12df98..5d2508705f 100644 --- a/tests/unit/storage/test_storage.py +++ b/tests/unit/storage/test_storage.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import shutil import tempfile +import threading from abc import ABC from dataclasses import dataclass, field from io import BytesIO, StringIO @@ -122,6 +123,57 @@ class TestLocalFileStore(TestCase, _StorageTest): f'Failed to remove temporary directory {self.temp_dir}: {e}' ) + def test_concurrent_writes_no_corruption(self): + """Test that concurrent writes don't corrupt file content. + + This test verifies the atomic write fix by having 9 threads write + progressively shorter strings to the same file simultaneously. + Without atomic writes, a shorter write following a longer write + could result in corrupted content (e.g., "123" followed by garbage + from the previous longer write). + + The final content must be exactly one of the valid strings written, + with no trailing garbage from other writes. + """ + filename = 'concurrent_test.txt' + # Strings from longest to shortest: "123456789", "12345678", ..., "1" + valid_contents = ['123456789'[:i] for i in range(9, 0, -1)] + errors: list[Exception] = [] + barrier = threading.Barrier(len(valid_contents)) + + def write_content(content: str): + try: + # Wait for all threads to be ready before writing + barrier.wait() + self.store.write(filename, content) + except Exception as e: + errors.append(e) + + # Start all threads + threads = [ + threading.Thread(target=write_content, args=(content,)) + for content in valid_contents + ] + for t in threads: + t.start() + for t in threads: + t.join() + + # Check for errors during writes + self.assertEqual( + errors, [], f'Errors occurred during concurrent writes: {errors}' + ) + + # Read final content and verify it's one of the valid strings + final_content = self.store.read(filename) + self.assertIn( + final_content, + valid_contents, + f"File content '{final_content}' is not one of the valid strings. " + f'Length: {len(final_content)}. This indicates file corruption from ' + f'concurrent writes (e.g., shorter write did not fully replace longer write).', + ) + class TestInMemoryFileStore(TestCase, _StorageTest): def setUp(self): From dcb2e21b87b87d9a52c23c8a44f3d907f0648f60 Mon Sep 17 00:00:00 2001 From: Saurya Velagapudi Date: Wed, 18 Mar 2026 17:07:19 -0700 Subject: [PATCH 20/21] feat: Auto-forward LLM_* env vars to agent-server and fix host network config (#13192) Co-authored-by: openhands --- .../sandbox/docker_sandbox_service.py | 21 +- .../sandbox/sandbox_spec_service.py | 53 ++- .../test_agent_server_env_override.py | 422 +++++++++++++++++- .../app_server/test_docker_sandbox_service.py | 53 +++ 4 files changed, 528 insertions(+), 21 deletions(-) diff --git a/openhands/app_server/sandbox/docker_sandbox_service.py b/openhands/app_server/sandbox/docker_sandbox_service.py index 6c692a680a..f5a302fa73 100644 --- a/openhands/app_server/sandbox/docker_sandbox_service.py +++ b/openhands/app_server/sandbox/docker_sandbox_service.py @@ -43,6 +43,16 @@ _logger = logging.getLogger(__name__) STARTUP_GRACE_SECONDS = 15 +def _get_use_host_network_default() -> bool: + """Get the default value for use_host_network from environment variables. + + This function is called at runtime (not at class definition time) to ensure + that environment variable changes are picked up correctly. + """ + value = os.getenv('AGENT_SERVER_USE_HOST_NETWORK', '') + return value.lower() in ('true', '1', 'yes') + + class VolumeMount(BaseModel): """Mounted volume within the container.""" @@ -591,18 +601,13 @@ class DockerSandboxServiceInjector(SandboxServiceInjector): ), ) use_host_network: bool = Field( - default=os.getenv('SANDBOX_USE_HOST_NETWORK', '').lower() - in ( - 'true', - '1', - 'yes', - ), + default_factory=_get_use_host_network_default, description=( - 'Whether to use host networking mode for sandbox containers. ' + 'Whether to use host networking mode for agent-server containers. ' 'When enabled, containers share the host network namespace, ' 'making all container ports directly accessible on the host. ' 'This is useful for reverse proxy setups where dynamic port mapping ' - 'is problematic. Configure via OH_SANDBOX_USE_HOST_NETWORK environment variable.' + 'is problematic. Configure via AGENT_SERVER_USE_HOST_NETWORK environment variable.' ), ) diff --git a/openhands/app_server/sandbox/sandbox_spec_service.py b/openhands/app_server/sandbox/sandbox_spec_service.py index 4034af1f5b..5025bdee6b 100644 --- a/openhands/app_server/sandbox/sandbox_spec_service.py +++ b/openhands/app_server/sandbox/sandbox_spec_service.py @@ -69,27 +69,58 @@ def get_agent_server_image() -> str: return AGENT_SERVER_IMAGE +# Prefixes for environment variables that should be auto-forwarded to agent-server +# These are typically configuration variables that affect the agent's behavior +AUTO_FORWARD_PREFIXES = ('LLM_',) + + def get_agent_server_env() -> dict[str, str]: """Get environment variables to be injected into agent server sandbox environments. - This function reads environment variable overrides from the OH_AGENT_SERVER_ENV - environment variable, which should contain a JSON string mapping variable names - to their values. + This function combines two sources of environment variables: + + 1. **Auto-forwarded variables**: Environment variables with certain prefixes + (e.g., LLM_*) are automatically forwarded to the agent-server container. + This ensures that LLM configuration like timeouts and retry settings + work correctly in the two-container V1 architecture. + + 2. **Explicit overrides via OH_AGENT_SERVER_ENV**: A JSON string that allows + setting arbitrary environment variables in the agent-server container. + Values set here take precedence over auto-forwarded variables. + + Auto-forwarded prefixes: + - LLM_* : LLM configuration (timeout, retries, model settings, etc.) Usage: - Set OH_AGENT_SERVER_ENV to a JSON string: - OH_AGENT_SERVER_ENV='{"DEBUG": "true", "LOG_LEVEL": "info", "CUSTOM_VAR": "value"}' + # Auto-forwarding (no action needed): + export LLM_TIMEOUT=3600 + export LLM_NUM_RETRIES=10 + # These will automatically be available in the agent-server - This will inject the following environment variables into all sandbox environments: - - DEBUG=true - - LOG_LEVEL=info - - CUSTOM_VAR=value + # Explicit override via JSON: + OH_AGENT_SERVER_ENV='{"DEBUG": "true", "CUSTOM_VAR": "value"}' + + # Override an auto-forwarded variable: + export LLM_TIMEOUT=3600 # Would be auto-forwarded as 3600 + OH_AGENT_SERVER_ENV='{"LLM_TIMEOUT": "7200"}' # Overrides to 7200 Returns: dict[str, str]: Dictionary of environment variable names to values. - Returns empty dict if OH_AGENT_SERVER_ENV is not set or invalid. + Returns empty dict if no variables are found. Raises: JSONDecodeError: If OH_AGENT_SERVER_ENV contains invalid JSON. """ - return env_parser.from_env(dict[str, str], 'OH_AGENT_SERVER_ENV') + result: dict[str, str] = {} + + # Step 1: Auto-forward environment variables with recognized prefixes + for key, value in os.environ.items(): + if any(key.startswith(prefix) for prefix in AUTO_FORWARD_PREFIXES): + result[key] = value + + # Step 2: Apply explicit overrides from OH_AGENT_SERVER_ENV + # These take precedence over auto-forwarded variables + explicit_env = env_parser.from_env(dict[str, str], 'OH_AGENT_SERVER_ENV') + result.update(explicit_env) + + return result diff --git a/tests/unit/app_server/test_agent_server_env_override.py b/tests/unit/app_server/test_agent_server_env_override.py index 61d851590e..5c1c1ea208 100644 --- a/tests/unit/app_server/test_agent_server_env_override.py +++ b/tests/unit/app_server/test_agent_server_env_override.py @@ -2,10 +2,11 @@ This module tests the environment variable override functionality that allows users to inject custom environment variables into sandbox environments via -OH_AGENT_SERVER_ENV_* environment variables. +OH_AGENT_SERVER_ENV environment variable and auto-forwarding of LLM_* variables. The functionality includes: -- Parsing OH_AGENT_SERVER_ENV_* environment variables +- Auto-forwarding of LLM_* environment variables to agent-server containers +- Explicit overrides via OH_AGENT_SERVER_ENV JSON - Merging them into sandbox specifications - Integration across different sandbox types (Docker, Process, Remote) """ @@ -25,6 +26,7 @@ from openhands.app_server.sandbox.remote_sandbox_spec_service import ( get_default_sandbox_specs as get_default_remote_sandbox_specs, ) from openhands.app_server.sandbox.sandbox_spec_service import ( + AUTO_FORWARD_PREFIXES, get_agent_server_env, ) @@ -185,6 +187,114 @@ class TestGetAgentServerEnv: assert result == expected +class TestLLMAutoForwarding: + """Test cases for automatic forwarding of LLM_* environment variables.""" + + def test_auto_forward_prefixes_contains_llm(self): + """Test that LLM_ is in the auto-forward prefixes.""" + assert 'LLM_' in AUTO_FORWARD_PREFIXES + + def test_llm_timeout_auto_forwarded(self): + """Test that LLM_TIMEOUT is automatically forwarded.""" + env_vars = { + 'LLM_TIMEOUT': '3600', + 'OTHER_VAR': 'should_not_be_included', + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + assert 'LLM_TIMEOUT' in result + assert result['LLM_TIMEOUT'] == '3600' + assert 'OTHER_VAR' not in result + + def test_llm_num_retries_auto_forwarded(self): + """Test that LLM_NUM_RETRIES is automatically forwarded.""" + env_vars = { + 'LLM_NUM_RETRIES': '10', + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + assert 'LLM_NUM_RETRIES' in result + assert result['LLM_NUM_RETRIES'] == '10' + + def test_multiple_llm_vars_auto_forwarded(self): + """Test that multiple LLM_* variables are automatically forwarded.""" + env_vars = { + 'LLM_TIMEOUT': '3600', + 'LLM_NUM_RETRIES': '10', + 'LLM_MODEL': 'gpt-4', + 'LLM_BASE_URL': 'https://api.example.com', + 'LLM_API_KEY': 'secret-key', + 'NON_LLM_VAR': 'should_not_be_included', + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + assert result['LLM_TIMEOUT'] == '3600' + assert result['LLM_NUM_RETRIES'] == '10' + assert result['LLM_MODEL'] == 'gpt-4' + assert result['LLM_BASE_URL'] == 'https://api.example.com' + assert result['LLM_API_KEY'] == 'secret-key' + assert 'NON_LLM_VAR' not in result + + def test_explicit_override_takes_precedence(self): + """Test that OH_AGENT_SERVER_ENV overrides auto-forwarded variables.""" + env_vars = { + 'LLM_TIMEOUT': '3600', # Auto-forwarded value + 'OH_AGENT_SERVER_ENV': '{"LLM_TIMEOUT": "7200"}', # Explicit override + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + # Explicit override should win + assert result['LLM_TIMEOUT'] == '7200' + + def test_combined_auto_forward_and_explicit(self): + """Test combining auto-forwarded and explicit variables.""" + env_vars = { + 'LLM_TIMEOUT': '3600', # Auto-forwarded + 'LLM_NUM_RETRIES': '10', # Auto-forwarded + 'OH_AGENT_SERVER_ENV': '{"DEBUG": "true", "CUSTOM_VAR": "value"}', # Explicit + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + # Auto-forwarded + assert result['LLM_TIMEOUT'] == '3600' + assert result['LLM_NUM_RETRIES'] == '10' + # Explicit + assert result['DEBUG'] == 'true' + assert result['CUSTOM_VAR'] == 'value' + + def test_no_llm_vars_returns_empty_without_explicit(self): + """Test that no LLM_* vars and no explicit env returns empty dict.""" + env_vars = { + 'SOME_OTHER_VAR': 'value', + 'ANOTHER_VAR': 'another_value', + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + assert result == {} + + def test_llm_prefix_is_case_sensitive(self): + """Test that LLM_ prefix matching is case-sensitive.""" + env_vars = { + 'LLM_TIMEOUT': '3600', # Should be included + 'llm_timeout': 'lowercase', # Should NOT be included (wrong case) + 'Llm_Timeout': 'mixed', # Should NOT be included (wrong case) + } + + with patch.dict(os.environ, env_vars, clear=True): + result = get_agent_server_env() + assert 'LLM_TIMEOUT' in result + assert result['LLM_TIMEOUT'] == '3600' + # Lowercase variants should not be included + assert 'llm_timeout' not in result + assert 'Llm_Timeout' not in result + + class TestDockerSandboxSpecEnvironmentOverride: """Test environment variable override integration in Docker sandbox specs.""" @@ -476,3 +586,311 @@ class TestEnvironmentOverrideIntegration: # Should not have the old variables assert 'VAR1' not in spec_2.initial_env assert 'VAR2' not in spec_2.initial_env + + +class TestDockerSandboxServiceEnvIntegration: + """Integration tests for environment variable propagation to Docker sandbox containers. + + These tests verify that environment variables are correctly propagated through + the entire flow from the app-server environment to the agent-server container. + """ + + def test_llm_env_vars_propagated_to_container_run(self): + """Test that LLM_* env vars are included in docker container.run() environment argument.""" + from unittest.mock import patch + + # Set up environment with LLM_* variables + env_vars = { + 'LLM_TIMEOUT': '3600', + 'LLM_NUM_RETRIES': '10', + 'LLM_MODEL': 'gpt-4', + 'OTHER_VAR': 'should_not_be_forwarded', + } + + with patch.dict(os.environ, env_vars, clear=True): + # Create a sandbox spec using the actual factory to get LLM_* vars + specs = get_default_docker_sandbox_specs() + sandbox_spec = specs[0] + + # Verify the sandbox spec has the LLM_* variables + assert 'LLM_TIMEOUT' in sandbox_spec.initial_env + assert sandbox_spec.initial_env['LLM_TIMEOUT'] == '3600' + assert 'LLM_NUM_RETRIES' in sandbox_spec.initial_env + assert sandbox_spec.initial_env['LLM_NUM_RETRIES'] == '10' + assert 'LLM_MODEL' in sandbox_spec.initial_env + assert sandbox_spec.initial_env['LLM_MODEL'] == 'gpt-4' + # Non-LLM_* variables should not be included + assert 'OTHER_VAR' not in sandbox_spec.initial_env + + def test_explicit_oh_agent_server_env_overrides_llm_vars(self): + """Test that OH_AGENT_SERVER_ENV can override auto-forwarded LLM_* variables.""" + env_vars = { + 'LLM_TIMEOUT': '3600', # Auto-forwarded value + 'OH_AGENT_SERVER_ENV': '{"LLM_TIMEOUT": "7200"}', # Override value + } + + with patch.dict(os.environ, env_vars, clear=True): + specs = get_default_docker_sandbox_specs() + sandbox_spec = specs[0] + + # OH_AGENT_SERVER_ENV should take precedence + assert sandbox_spec.initial_env['LLM_TIMEOUT'] == '7200' + + def test_multiple_llm_vars_combined_with_explicit_overrides(self): + """Test complex scenario with multiple LLM_* vars and explicit overrides.""" + env_vars = { + 'LLM_TIMEOUT': '3600', + 'LLM_NUM_RETRIES': '10', + 'LLM_MODEL': 'gpt-4', + 'LLM_TEMPERATURE': '0.7', + 'OH_AGENT_SERVER_ENV': '{"LLM_MODEL": "gpt-3.5-turbo", "CUSTOM_VAR": "custom_value"}', + } + + with patch.dict(os.environ, env_vars, clear=True): + specs = get_default_docker_sandbox_specs() + sandbox_spec = specs[0] + + # Auto-forwarded LLM_* vars that weren't overridden + assert sandbox_spec.initial_env['LLM_TIMEOUT'] == '3600' + assert sandbox_spec.initial_env['LLM_NUM_RETRIES'] == '10' + assert sandbox_spec.initial_env['LLM_TEMPERATURE'] == '0.7' + + # LLM_MODEL should be overridden by OH_AGENT_SERVER_ENV + assert sandbox_spec.initial_env['LLM_MODEL'] == 'gpt-3.5-turbo' + + # Custom variable from OH_AGENT_SERVER_ENV + assert sandbox_spec.initial_env['CUSTOM_VAR'] == 'custom_value' + + def test_sandbox_spec_env_passed_to_docker_container_run(self): + """Test that sandbox spec's initial_env is passed to docker container run.""" + from unittest.mock import AsyncMock, MagicMock, patch + + import httpx + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxService, + ExposedPort, + ) + + # Create mock docker client + mock_docker_client = MagicMock() + mock_container = MagicMock() + mock_container.name = 'oh-test-abc123' + mock_container.image.tags = ['test-image:latest'] + mock_container.attrs = { + 'Created': '2024-01-01T00:00:00Z', + 'Config': { + 'Env': ['SESSION_API_KEY=test-key'], + 'WorkingDir': '/workspace', + }, + 'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': '32768'}]}}, + 'HostConfig': {'NetworkMode': 'bridge'}, + } + mock_container.status = 'running' + mock_docker_client.containers.run.return_value = mock_container + mock_docker_client.containers.list.return_value = [] + + # Create mock sandbox spec service + mock_spec_service = MagicMock() + + # Create sandbox spec with LLM_* environment variables + env_vars = { + 'LLM_TIMEOUT': '3600', + 'LLM_NUM_RETRIES': '10', + } + + with patch.dict(os.environ, env_vars, clear=True): + specs = get_default_docker_sandbox_specs() + sandbox_spec = specs[0] + + mock_spec_service.get_default_sandbox_spec = AsyncMock( + return_value=sandbox_spec + ) + + # Create service + service = DockerSandboxService( + sandbox_spec_service=mock_spec_service, + container_name_prefix='oh-test-', + host_port=3000, + container_url_pattern='http://localhost:{port}', + mounts=[], + exposed_ports=[ + ExposedPort( + name='AGENT_SERVER', + description='Agent server', + container_port=8000, + ) + ], + health_check_path='/health', + httpx_client=MagicMock(spec=httpx.AsyncClient), + max_num_sandboxes=5, + docker_client=mock_docker_client, + ) + + # Start sandbox + import asyncio + + asyncio.get_event_loop().run_until_complete(service.start_sandbox()) + + # Verify docker was called with environment variables including LLM_* + call_kwargs = mock_docker_client.containers.run.call_args[1] + container_env = call_kwargs['environment'] + + # LLM_* variables should be in the container environment + assert 'LLM_TIMEOUT' in container_env + assert container_env['LLM_TIMEOUT'] == '3600' + assert 'LLM_NUM_RETRIES' in container_env + assert container_env['LLM_NUM_RETRIES'] == '10' + + # Default variables should also be present + assert 'OPENVSCODE_SERVER_ROOT' in container_env + assert 'LOG_JSON' in container_env + + def test_host_network_mode_with_env_var(self): + """Test that AGENT_SERVER_USE_HOST_NETWORK affects container network mode.""" + from unittest.mock import AsyncMock, MagicMock, patch + + import httpx + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxService, + ExposedPort, + _get_use_host_network_default, + ) + + # Test with environment variable set + with patch.dict( + os.environ, {'AGENT_SERVER_USE_HOST_NETWORK': 'true'}, clear=True + ): + assert _get_use_host_network_default() is True + + # Create mock docker client + mock_docker_client = MagicMock() + mock_container = MagicMock() + mock_container.name = 'oh-test-abc123' + mock_container.image.tags = ['test-image:latest'] + mock_container.attrs = { + 'Created': '2024-01-01T00:00:00Z', + 'Config': { + 'Env': ['SESSION_API_KEY=test-key'], + 'WorkingDir': '/workspace', + }, + 'NetworkSettings': {'Ports': {}}, + 'HostConfig': {'NetworkMode': 'host'}, + } + mock_container.status = 'running' + mock_docker_client.containers.run.return_value = mock_container + mock_docker_client.containers.list.return_value = [] + + # Create mock sandbox spec service + mock_spec_service = MagicMock() + specs = get_default_docker_sandbox_specs() + mock_spec_service.get_default_sandbox_spec = AsyncMock( + return_value=specs[0] + ) + + # Create service with host network enabled + service = DockerSandboxService( + sandbox_spec_service=mock_spec_service, + container_name_prefix='oh-test-', + host_port=3000, + container_url_pattern='http://localhost:{port}', + mounts=[], + exposed_ports=[ + ExposedPort( + name='AGENT_SERVER', + description='Agent server', + container_port=8000, + ) + ], + health_check_path='/health', + httpx_client=MagicMock(spec=httpx.AsyncClient), + max_num_sandboxes=5, + docker_client=mock_docker_client, + use_host_network=True, + ) + + # Start sandbox + import asyncio + + asyncio.get_event_loop().run_until_complete(service.start_sandbox()) + + # Verify docker was called with host network mode + call_kwargs = mock_docker_client.containers.run.call_args[1] + assert call_kwargs['network_mode'] == 'host' + # Port mappings should be None in host network mode + assert call_kwargs['ports'] is None + + def test_bridge_network_mode_without_env_var(self): + """Test that default (bridge) network mode is used when env var is not set.""" + from unittest.mock import AsyncMock, MagicMock, patch + + import httpx + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxService, + ExposedPort, + _get_use_host_network_default, + ) + + # Test without environment variable + with patch.dict(os.environ, {}, clear=True): + assert _get_use_host_network_default() is False + + # Create mock docker client + mock_docker_client = MagicMock() + mock_container = MagicMock() + mock_container.name = 'oh-test-abc123' + mock_container.image.tags = ['test-image:latest'] + mock_container.attrs = { + 'Created': '2024-01-01T00:00:00Z', + 'Config': { + 'Env': ['SESSION_API_KEY=test-key'], + 'WorkingDir': '/workspace', + }, + 'NetworkSettings': {'Ports': {'8000/tcp': [{'HostPort': '32768'}]}}, + 'HostConfig': {'NetworkMode': 'bridge'}, + } + mock_container.status = 'running' + mock_docker_client.containers.run.return_value = mock_container + mock_docker_client.containers.list.return_value = [] + + # Create mock sandbox spec service + mock_spec_service = MagicMock() + specs = get_default_docker_sandbox_specs() + mock_spec_service.get_default_sandbox_spec = AsyncMock( + return_value=specs[0] + ) + + # Create service with bridge network (default) + service = DockerSandboxService( + sandbox_spec_service=mock_spec_service, + container_name_prefix='oh-test-', + host_port=3000, + container_url_pattern='http://localhost:{port}', + mounts=[], + exposed_ports=[ + ExposedPort( + name='AGENT_SERVER', + description='Agent server', + container_port=8000, + ) + ], + health_check_path='/health', + httpx_client=MagicMock(spec=httpx.AsyncClient), + max_num_sandboxes=5, + docker_client=mock_docker_client, + use_host_network=False, + ) + + # Start sandbox + import asyncio + + asyncio.get_event_loop().run_until_complete(service.start_sandbox()) + + # Verify docker was called with bridge network mode (network_mode=None) + call_kwargs = mock_docker_client.containers.run.call_args[1] + assert call_kwargs['network_mode'] is None + # Port mappings should be present in bridge mode + assert call_kwargs['ports'] is not None + assert 8000 in call_kwargs['ports'] diff --git a/tests/unit/app_server/test_docker_sandbox_service.py b/tests/unit/app_server/test_docker_sandbox_service.py index 23a6d51b04..f6ae716eef 100644 --- a/tests/unit/app_server/test_docker_sandbox_service.py +++ b/tests/unit/app_server/test_docker_sandbox_service.py @@ -1254,6 +1254,59 @@ class TestDockerSandboxServiceInjector: injector = DockerSandboxServiceInjector(use_host_network=True) assert injector.use_host_network is True + def test_use_host_network_from_agent_server_env_var(self): + """Test that AGENT_SERVER_USE_HOST_NETWORK env var enables host network mode.""" + import os + from unittest.mock import patch + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxServiceInjector, + ) + + env_vars = { + 'AGENT_SERVER_USE_HOST_NETWORK': 'true', + } + + with patch.dict(os.environ, env_vars, clear=True): + injector = DockerSandboxServiceInjector() + assert injector.use_host_network is True + + def test_use_host_network_env_var_accepts_various_true_values(self): + """Test that use_host_network accepts various truthy values.""" + import os + from unittest.mock import patch + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxServiceInjector, + ) + + for true_value in ['true', 'TRUE', 'True', '1', 'yes', 'YES', 'Yes']: + env_vars = {'AGENT_SERVER_USE_HOST_NETWORK': true_value} + with patch.dict(os.environ, env_vars, clear=True): + injector = DockerSandboxServiceInjector() + assert injector.use_host_network is True, ( + f'Failed for value: {true_value}' + ) + + def test_use_host_network_env_var_defaults_to_false(self): + """Test that unset or empty env var defaults to False.""" + import os + from unittest.mock import patch + + from openhands.app_server.sandbox.docker_sandbox_service import ( + DockerSandboxServiceInjector, + ) + + # Empty environment + with patch.dict(os.environ, {}, clear=True): + injector = DockerSandboxServiceInjector() + assert injector.use_host_network is False + + # Empty string + with patch.dict(os.environ, {'AGENT_SERVER_USE_HOST_NETWORK': ''}, clear=True): + injector = DockerSandboxServiceInjector() + assert injector.use_host_network is False + class TestDockerSandboxServiceInjectorFromEnv: """Test cases for DockerSandboxServiceInjector environment variable configuration.""" From a96760eea70fa99b1bcb73bd6a35171c596366d1 Mon Sep 17 00:00:00 2001 From: Saurya Velagapudi Date: Wed, 18 Mar 2026 17:16:43 -0700 Subject: [PATCH 21/21] fix: ensure LiteLLM user exists before generating API keys (#12667) Co-authored-by: openhands --- enterprise/storage/lite_llm_manager.py | 84 +++++- .../tests/unit/test_lite_llm_manager.py | 284 ++++++++++++++++-- 2 files changed, 331 insertions(+), 37 deletions(-) diff --git a/enterprise/storage/lite_llm_manager.py b/enterprise/storage/lite_llm_manager.py index 725b8147a3..b515b7a7d9 100644 --- a/enterprise/storage/lite_llm_manager.py +++ b/enterprise/storage/lite_llm_manager.py @@ -164,9 +164,33 @@ class LiteLlmManager: ) if create_user: - await LiteLlmManager._create_user( + user_created = await LiteLlmManager._create_user( client, keycloak_user_info.get('email'), keycloak_user_id ) + if not user_created: + logger.error( + 'create_entries_failed_user_creation', + extra={ + 'org_id': org_id, + 'user_id': keycloak_user_id, + }, + ) + return None + + # Verify user exists before proceeding with key generation + user_exists = await LiteLlmManager._user_exists( + client, keycloak_user_id + ) + if not user_exists: + logger.error( + 'create_entries_user_not_found_before_key_generation', + extra={ + 'org_id': org_id, + 'user_id': keycloak_user_id, + 'create_user_flag': create_user, + }, + ) + return None await LiteLlmManager._add_user_to_team( client, keycloak_user_id, org_id, team_budget @@ -655,15 +679,48 @@ class LiteLlmManager: ) response.raise_for_status() + @staticmethod + async def _user_exists( + client: httpx.AsyncClient, + user_id: str, + ) -> bool: + """Check if a user exists in LiteLLM. + + Returns True if the user exists, False otherwise. + """ + if LITE_LLM_API_KEY is None or LITE_LLM_API_URL is None: + return False + try: + response = await client.get( + f'{LITE_LLM_API_URL}/user/info?user_id={user_id}', + ) + if response.is_success: + user_data = response.json() + # Check that user_info exists and has the user_id + user_info = user_data.get('user_info', {}) + return user_info.get('user_id') == user_id + return False + except Exception as e: + logger.warning( + 'litellm_user_exists_check_failed', + extra={'user_id': user_id, 'error': str(e)}, + ) + return False + @staticmethod async def _create_user( client: httpx.AsyncClient, email: str | None, keycloak_user_id: str, - ): + ) -> bool: + """Create a user in LiteLLM. + + Returns True if the user was created or already exists and is verified, + False if creation failed and user does not exist. + """ if LITE_LLM_API_KEY is None or LITE_LLM_API_URL is None: logger.warning('LiteLLM API configuration not found') - return + return False response = await client.post( f'{LITE_LLM_API_URL}/user/new', json={ @@ -716,17 +773,33 @@ class LiteLlmManager: 'user_id': keycloak_user_id, }, ) - return + # Verify the user actually exists before returning success + user_exists = await LiteLlmManager._user_exists( + client, keycloak_user_id + ) + if not user_exists: + logger.error( + 'litellm_user_claimed_exists_but_not_found', + extra={ + 'user_id': keycloak_user_id, + 'status_code': response.status_code, + 'text': response.text, + }, + ) + return False + return True logger.error( 'error_creating_litellm_user', extra={ 'status_code': response.status_code, 'text': response.text, - 'user_id': [keycloak_user_id], + 'user_id': keycloak_user_id, 'email': None, }, ) + return False response.raise_for_status() + return True @staticmethod async def _get_user(client: httpx.AsyncClient, user_id: str) -> dict | None: @@ -1450,6 +1523,7 @@ class LiteLlmManager: create_team = staticmethod(with_http_client(_create_team)) get_team = staticmethod(with_http_client(_get_team)) update_team = staticmethod(with_http_client(_update_team)) + user_exists = staticmethod(with_http_client(_user_exists)) create_user = staticmethod(with_http_client(_create_user)) get_user = staticmethod(with_http_client(_get_user)) update_user = staticmethod(with_http_client(_update_user)) diff --git a/enterprise/tests/unit/test_lite_llm_manager.py b/enterprise/tests/unit/test_lite_llm_manager.py index 0cfc9fe58b..3da159421d 100644 --- a/enterprise/tests/unit/test_lite_llm_manager.py +++ b/enterprise/tests/unit/test_lite_llm_manager.py @@ -239,6 +239,16 @@ class TestLiteLlmManager: mock_404_response = MagicMock() mock_404_response.status_code = 404 mock_404_response.is_success = False + mock_404_response.raise_for_status.side_effect = httpx.HTTPStatusError( + message='Not Found', request=MagicMock(), response=mock_404_response + ) + + # Mock user exists check response + mock_user_exists_response = MagicMock() + mock_user_exists_response.is_success = True + mock_user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } mock_token_manager = MagicMock() mock_token_manager.return_value.get_user_info_from_user_id = AsyncMock( @@ -246,12 +256,8 @@ class TestLiteLlmManager: ) mock_client = AsyncMock() - mock_client.get.return_value = mock_404_response - mock_client.get.return_value.raise_for_status.side_effect = ( - httpx.HTTPStatusError( - message='Not Found', request=MagicMock(), response=mock_404_response - ) - ) + # First GET is for _get_team (404), second GET is for _user_exists (success) + mock_client.get.side_effect = [mock_404_response, mock_user_exists_response] mock_client.post.return_value = mock_response mock_client_class = MagicMock() @@ -274,8 +280,8 @@ class TestLiteLlmManager: assert result.llm_api_key.get_secret_value() == 'test-api-key' assert result.llm_base_url == 'http://test.com' - # Verify API calls were made (get_team + 4 posts) - assert mock_client.get.call_count == 1 # get_team + # Verify API calls were made (get_team + user_exists + 4 posts) + assert mock_client.get.call_count == 2 # get_team + user_exists assert ( mock_client.post.call_count == 4 ) # create_team, add_user_to_team, delete_key_by_alias, generate_key @@ -294,13 +300,21 @@ class TestLiteLlmManager: } mock_team_response.raise_for_status = MagicMock() + # Mock user exists check response + mock_user_exists_response = MagicMock() + mock_user_exists_response.is_success = True + mock_user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } + mock_token_manager = MagicMock() mock_token_manager.return_value.get_user_info_from_user_id = AsyncMock( return_value={'email': 'test@example.com'} ) mock_client = AsyncMock() - mock_client.get.return_value = mock_team_response + # First GET is for _get_team (success), second GET is for _user_exists (success) + mock_client.get.side_effect = [mock_team_response, mock_user_exists_response] mock_client.post.return_value = mock_response mock_client_class = MagicMock() @@ -320,8 +334,8 @@ class TestLiteLlmManager: assert result is not None # Verify _get_team was called first - mock_client.get.assert_called_once() - get_call_url = mock_client.get.call_args[0][0] + assert mock_client.get.call_count == 2 # get_team + user_exists + get_call_url = mock_client.get.call_args_list[0][0][0] assert 'team/info' in get_call_url assert 'test-org-id' in get_call_url @@ -343,19 +357,25 @@ class TestLiteLlmManager: mock_404_response = MagicMock() mock_404_response.status_code = 404 mock_404_response.is_success = False + mock_404_response.raise_for_status.side_effect = httpx.HTTPStatusError( + message='Not Found', request=MagicMock(), response=mock_404_response + ) mock_token_manager = MagicMock() mock_token_manager.return_value.get_user_info_from_user_id = AsyncMock( return_value={'email': 'test@example.com'} ) + # Mock user exists check response + mock_user_exists_response = MagicMock() + mock_user_exists_response.is_success = True + mock_user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } + mock_client = AsyncMock() - mock_client.get.return_value = mock_404_response - mock_client.get.return_value.raise_for_status.side_effect = ( - httpx.HTTPStatusError( - message='Not Found', request=MagicMock(), response=mock_404_response - ) - ) + # First GET is for _get_team (404), second GET is for _user_exists (success) + mock_client.get.side_effect = [mock_404_response, mock_user_exists_response] mock_client.post.return_value = mock_response mock_client_class = MagicMock() @@ -393,6 +413,16 @@ class TestLiteLlmManager: mock_404_response = MagicMock() mock_404_response.status_code = 404 mock_404_response.is_success = False + mock_404_response.raise_for_status.side_effect = httpx.HTTPStatusError( + message='Not Found', request=MagicMock(), response=mock_404_response + ) + + # Mock user exists check response + mock_user_exists_response = MagicMock() + mock_user_exists_response.is_success = True + mock_user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } mock_token_manager = MagicMock() mock_token_manager.return_value.get_user_info_from_user_id = AsyncMock( @@ -400,12 +430,8 @@ class TestLiteLlmManager: ) mock_client = AsyncMock() - mock_client.get.return_value = mock_404_response - mock_client.get.return_value.raise_for_status.side_effect = ( - httpx.HTTPStatusError( - message='Not Found', request=MagicMock(), response=mock_404_response - ) - ) + # First GET is for _get_team (404), second GET is for _user_exists (success) + mock_client.get.side_effect = [mock_404_response, mock_user_exists_response] mock_client.post.return_value = mock_response mock_client_class = MagicMock() @@ -833,15 +859,16 @@ class TestLiteLlmManager: @pytest.mark.asyncio async def test_create_user_success(self, mock_http_client, mock_response): - """Test successful _create_user operation.""" + """Test successful _create_user operation returns True.""" mock_http_client.post.return_value = mock_response with patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key'): with patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com'): - await LiteLlmManager._create_user( + result = await LiteLlmManager._create_user( mock_http_client, 'test@example.com', 'test-user-id' ) + assert result is True mock_http_client.post.assert_called_once() call_args = mock_http_client.post.call_args assert 'http://test.com/user/new' in call_args[0] @@ -850,7 +877,7 @@ class TestLiteLlmManager: @pytest.mark.asyncio async def test_create_user_duplicate_email(self, mock_http_client, mock_response): - """Test _create_user with duplicate email handling.""" + """Test _create_user with duplicate email handling returns True.""" # First call fails with duplicate email error_response = MagicMock() error_response.is_success = False @@ -862,23 +889,81 @@ class TestLiteLlmManager: with patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key'): with patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com'): - await LiteLlmManager._create_user( + result = await LiteLlmManager._create_user( mock_http_client, 'test@example.com', 'test-user-id' ) + assert result is True assert mock_http_client.post.call_count == 2 # Second call should have None email second_call_args = mock_http_client.post.call_args_list[1] assert second_call_args[1]['json']['user_email'] is None + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_user_exists_returns_true(self, mock_http_client): + """Test _user_exists returns True when user exists in LiteLLM.""" + # Arrange + user_response = MagicMock() + user_response.is_success = True + user_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id', 'email': 'test@example.com'} + } + mock_http_client.get.return_value = user_response + + # Act + result = await LiteLlmManager._user_exists(mock_http_client, 'test-user-id') + + # Assert + assert result is True + mock_http_client.get.assert_called_once() + + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_user_exists_returns_false_when_not_found(self, mock_http_client): + """Test _user_exists returns False when user not found.""" + # Arrange + user_response = MagicMock() + user_response.is_success = False + mock_http_client.get.return_value = user_response + + # Act + result = await LiteLlmManager._user_exists(mock_http_client, 'test-user-id') + + # Assert + assert result is False + + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_user_exists_returns_false_on_mismatched_user_id( + self, mock_http_client + ): + """Test _user_exists returns False when returned user_id doesn't match.""" + # Arrange + user_response = MagicMock() + user_response.is_success = True + user_response.json.return_value = { + 'user_info': {'user_id': 'different-user-id'} + } + mock_http_client.get.return_value = user_response + + # Act + result = await LiteLlmManager._user_exists(mock_http_client, 'test-user-id') + + # Assert + assert result is False + @pytest.mark.asyncio @patch('storage.lite_llm_manager.logger') @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') - async def test_create_user_already_exists_with_409_status_code( + async def test_create_user_already_exists_and_verified( self, mock_logger, mock_http_client ): - """Test _create_user handles 409 Conflict when user already exists.""" + """Test _create_user returns True when user already exists and is verified.""" # Arrange first_response = MagicMock() first_response.is_success = False @@ -890,14 +975,141 @@ class TestLiteLlmManager: second_response.status_code = 409 second_response.text = 'User with id test-user-id already exists' + user_exists_response = MagicMock() + user_exists_response.is_success = True + user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } + mock_http_client.post.side_effect = [first_response, second_response] + mock_http_client.get.return_value = user_exists_response # Act - await LiteLlmManager._create_user( + result = await LiteLlmManager._create_user( mock_http_client, 'test@example.com', 'test-user-id' ) # Assert + assert result is True + mock_logger.warning.assert_any_call( + 'litellm_user_already_exists', + extra={'user_id': 'test-user-id'}, + ) + + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.logger') + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_create_user_already_exists_but_not_found_returns_false( + self, mock_logger, mock_http_client + ): + """Test _create_user returns False when LiteLLM claims user exists but verification fails.""" + # Arrange + first_response = MagicMock() + first_response.is_success = False + first_response.status_code = 400 + first_response.text = 'duplicate email' + + second_response = MagicMock() + second_response.is_success = False + second_response.status_code = 409 + second_response.text = 'User with id test-user-id already exists' + + user_not_exists_response = MagicMock() + user_not_exists_response.is_success = False + + mock_http_client.post.side_effect = [first_response, second_response] + mock_http_client.get.return_value = user_not_exists_response + + # Act + result = await LiteLlmManager._create_user( + mock_http_client, 'test@example.com', 'test-user-id' + ) + + # Assert + assert result is False + mock_logger.error.assert_any_call( + 'litellm_user_claimed_exists_but_not_found', + extra={ + 'user_id': 'test-user-id', + 'status_code': 409, + 'text': 'User with id test-user-id already exists', + }, + ) + + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.logger') + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_create_user_failure_returns_false( + self, mock_logger, mock_http_client + ): + """Test _create_user returns False when creation fails with non-'already exists' error.""" + # Arrange + first_response = MagicMock() + first_response.is_success = False + first_response.status_code = 400 + first_response.text = 'duplicate email' + + second_response = MagicMock() + second_response.is_success = False + second_response.status_code = 500 + second_response.text = 'Internal server error' + + mock_http_client.post.side_effect = [first_response, second_response] + + # Act + result = await LiteLlmManager._create_user( + mock_http_client, 'test@example.com', 'test-user-id' + ) + + # Assert + assert result is False + mock_logger.error.assert_any_call( + 'error_creating_litellm_user', + extra={ + 'status_code': 500, + 'text': 'Internal server error', + 'user_id': 'test-user-id', + 'email': None, + }, + ) + + @pytest.mark.asyncio + @patch('storage.lite_llm_manager.logger') + @patch('storage.lite_llm_manager.LITE_LLM_API_URL', 'http://test.com') + @patch('storage.lite_llm_manager.LITE_LLM_API_KEY', 'test-key') + async def test_create_user_already_exists_with_409_status_code( + self, mock_logger, mock_http_client + ): + """Test _create_user handles 409 Conflict when user already exists and verifies.""" + # Arrange + first_response = MagicMock() + first_response.is_success = False + first_response.status_code = 400 + first_response.text = 'duplicate email' + + second_response = MagicMock() + second_response.is_success = False + second_response.status_code = 409 + second_response.text = 'User with id test-user-id already exists' + + user_exists_response = MagicMock() + user_exists_response.is_success = True + user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } + + mock_http_client.post.side_effect = [first_response, second_response] + mock_http_client.get.return_value = user_exists_response + + # Act + result = await LiteLlmManager._create_user( + mock_http_client, 'test@example.com', 'test-user-id' + ) + + # Assert + assert result is True mock_logger.warning.assert_any_call( 'litellm_user_already_exists', extra={'user_id': 'test-user-id'}, @@ -910,7 +1122,7 @@ class TestLiteLlmManager: async def test_create_user_already_exists_with_400_status_code( self, mock_logger, mock_http_client ): - """Test _create_user handles 400 Bad Request when user already exists.""" + """Test _create_user handles 400 Bad Request when user already exists and verifies.""" # Arrange first_response = MagicMock() first_response.is_success = False @@ -922,14 +1134,22 @@ class TestLiteLlmManager: second_response.status_code = 400 second_response.text = 'User already exists' + user_exists_response = MagicMock() + user_exists_response.is_success = True + user_exists_response.json.return_value = { + 'user_info': {'user_id': 'test-user-id'} + } + mock_http_client.post.side_effect = [first_response, second_response] + mock_http_client.get.return_value = user_exists_response # Act - await LiteLlmManager._create_user( + result = await LiteLlmManager._create_user( mock_http_client, 'test@example.com', 'test-user-id' ) # Assert + assert result is True mock_logger.warning.assert_any_call( 'litellm_user_already_exists', extra={'user_id': 'test-user-id'},