Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function DeleteConfirmationModal({
type="button"
onClick={onCancel}
className="absolute right-4 top-4 text-muted hover:text-foreground"
aria-label="Close"
aria-label={t(I18nKey.BUTTON$CLOSE)}
>
<XMarkIcon className="size-5" />
</button>
Expand Down
4 changes: 2 additions & 2 deletions src/components/features/backends/backend-form-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export function BackendForm({
setConnectionError(null);
}}
onBlur={() => setNameTouched(true)}
placeholder="Production"
placeholder={t(I18nKey.BACKEND$NAME_PLACEHOLDER)}
className="w-full"
showRequiredTag
error={nameError}
Expand Down Expand Up @@ -624,7 +624,7 @@ function ManualConnectionColumn({ onClose }: { onClose: () => void }) {
setName(value);
setConnectionError(null);
}}
placeholder="e.g. My Server"
placeholder={t(I18nKey.BACKEND$ADD_NAME_PLACEHOLDER)}
className="w-full"
/>
<p className="text-xs text-[var(--oh-muted)]">
Expand Down
7 changes: 5 additions & 2 deletions src/components/features/chat/chat-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import { useLlmConfigured } from "#/hooks/use-llm-configured";
import { Messages } from "#/components/conversation-events/chat/messages";
import { PendingUserMessages } from "./pending-user-messages";
import { useUnifiedUploadFiles } from "#/hooks/mutation/use-unified-upload-files";
import { validateFiles } from "#/utils/file-validation";
import {
formatFileValidationError,
validateFiles,
} from "#/utils/file-validation";
import { useConversationStore } from "#/stores/conversation-store";
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
import { useTaskPolling } from "#/hooks/query/use-task-polling";
Expand Down Expand Up @@ -307,7 +310,7 @@ export function ChatInterface() {
const validation = validateFiles(allFiles);

if (!validation.isValid) {
displayErrorToast(`Error: ${validation.errorMessage}`);
displayErrorToast(formatFileValidationError(validation, t));
return; // Stop processing if validation fails
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/features/chat/chat-messages-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";

const SKELETON_PATTERN = [
{ width: "w-[25%]", height: "h-4", align: "justify-end" },
Expand All @@ -22,11 +24,13 @@ function SkeletonBlock({ width, height }: { width: string; height: string }) {
}

export function ChatMessagesSkeleton() {
const { t } = useTranslation("openhands");

return (
<div
className="flex flex-col gap-6 p-4 w-full h-full overflow-hidden"
data-testid="chat-messages-skeleton"
aria-label="Loading conversation"
aria-label={t(I18nKey.CHAT_INTERFACE$LOADING_CONVERSATION)}
>
{SKELETON_PATTERN.map((item, i) => (
<div key={i} className={cn("flex w-full", item.align)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export function ChatInputActions({
ref={overflowTriggerRef}
type="button"
className={cn(chatInputIconButtonClassName, "size-6")}
aria-label="More input actions"
aria-label={t(I18nKey.CHAT_INTERFACE$MORE_INPUT_ACTIONS)}
aria-expanded={isOverflowOpen}
aria-haspopup="menu"
onClick={(event) => {
Expand Down
9 changes: 5 additions & 4 deletions src/components/features/chat/plan-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { useScrollContext } from "#/context/scroll-context";

const MAX_CONTENT_LENGTH = 300;
const BUILD_SHORTCUT_LABEL = String.fromCharCode(0x2318, 0x21a9);

// Shine effect class for streaming text
const SHINE_TEXT_CLASS = "shine-text";
Expand All @@ -31,7 +32,6 @@ interface PlanPreviewProps {
isBuildDisabled?: boolean;
}

/* eslint-disable i18next/no-literal-string */
export function PlanPreview({
planContent,
isStreaming,
Expand Down Expand Up @@ -128,12 +128,13 @@ export function PlanPreview({
: "hover:opacity-90 cursor-pointer",
)}
data-testid="plan-preview-build-button"
aria-label={t(I18nKey.COMMON$BUILD)}
>
<Typography.Text className="font-normal text-[14px] text-black leading-5">
{t(I18nKey.COMMON$BUILD)}{" "}
<Typography.Text className="font-normal text-black">
⌘↩
</Typography.Text>
<span className="font-normal text-black" aria-hidden="true">
{BUILD_SHORTCUT_LABEL}
</span>
</Typography.Text>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { AgentState } from "#/types/agent-state";
import { Typography } from "#/ui/typography";
import { mobileTopBarIconClassName } from "#/utils/mobile-top-bar-icon-button-classes";

const BUILD_SHORTCUT_LABEL = String.fromCharCode(0x2318, 0x21a9);

export function ConversationTabs({
variant = "default",
isPanelResizing = false,
Expand Down Expand Up @@ -366,10 +368,11 @@ export function ConversationTabs({
: "cursor-pointer hover:opacity-90",
)}
data-testid="planner-tab-build-button"
aria-label={t(I18nKey.COMMON$BUILD)}
>
<Typography.Text className="text-[11px] font-normal leading-5 text-black">
{/* eslint-disable-next-line i18next/no-literal-string */}
{t(I18nKey.COMMON$BUILD)} ⌘↩
{t(I18nKey.COMMON$BUILD)}{" "}
<span aria-hidden="true">{BUILD_SHORTCUT_LABEL}</span>
</Typography.Text>
</button>
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/components/features/diff-viewer/loading-spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";

export interface LoadingSpinnerProps {
className?: string;
}

export function LoadingSpinner({ className }: LoadingSpinnerProps) {
const { t } = useTranslation("openhands");

return (
<div className="flex items-center justify-center">
<div
Expand All @@ -13,7 +17,7 @@ export function LoadingSpinner({ className }: LoadingSpinnerProps) {
className,
)}
role="status"
aria-label="Loading"
aria-label={t(I18nKey.HOME$LOADING)}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export function GitBranchDropdown({
selectedBranch,
);

const error = isError ? new Error("Failed to load branches") : null;
const error = isError
? new Error(t(I18nKey.HOME$FAILED_TO_LOAD_BRANCHES))
: null;

// Handle clear
const handleClear = useCallback(() => {
Expand Down
6 changes: 5 additions & 1 deletion src/components/features/home/shared/clear-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";

interface ClearButtonProps {
Expand All @@ -12,6 +14,8 @@ export function ClearButton({
onClear,
testId = "dropdown-clear",
}: ClearButtonProps) {
const { t } = useTranslation("openhands");

return (
<button
onClick={(e) => {
Expand All @@ -24,7 +28,7 @@ export function ClearButton({
"cursor-pointer disabled:cursor-not-allowed disabled:opacity-60",
)}
type="button"
aria-label="Clear selection"
aria-label={t(I18nKey.COMMON$CLEAR_SELECTION)}
data-testid={testId}
>
<svg
Expand Down
6 changes: 5 additions & 1 deletion src/components/features/home/shared/toggle-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
ComboboxCaretIcon,
comboboxCaretButtonClassName,
} from "#/ui/combobox-caret";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";

interface ToggleButtonProps {
Expand All @@ -19,6 +21,8 @@ export function ToggleButton({
getToggleButtonProps,
iconClassName,
}: ToggleButtonProps) {
const { t } = useTranslation("openhands");

return (
<button
{...getToggleButtonProps({
Expand All @@ -31,7 +35,7 @@ export function ToggleButton({
),
})}
type="button"
aria-label="Toggle menu"
aria-label={t(I18nKey.COMMON$TOGGLE_MENU)}
>
<ComboboxCaretIcon className={iconClassName} />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export function FolderBrowserModal({
data-testid="folder-browser-up"
onClick={() => parent && setCurrentPath(parent)}
disabled={!parent}
aria-label="Up"
aria-label={t(I18nKey.COMMON$UP)}
className="p-1 rounded hover:bg-[var(--oh-interactive-hover)] text-white disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
<ChevronLeft width={16} height={16} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/features/settings/mobile-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function MobileHeader({
type="button"
onClick={onToggleMenu}
className="p-2 rounded-md bg-tertiary hover:bg-tertiary transition-colors"
aria-label="Toggle settings menu"
aria-label={t(I18nKey.SETTINGS$TOGGLE_MENU)}
>
<svg
width={20}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ export function SecretForm({
testId="name-input"
name="secret-name"
type="text"
label="Name"
label={t(I18nKey.SETTINGS$NAME)}
className="w-full min-w-0"
required
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
placeholder={t(I18nKey.SECRETS$API_KEY_EXAMPLE)}
pattern="^[a-zA-Z][a-zA-Z0-9_]{0,63}$"
title="Must start with a letter, contain only letters/numbers/underscores, and be 1-64 characters"
title={t(I18nKey.SECRETS$NAME_PATTERN_HELP)}
/>
{error && <p className="text-red-500 text-sm">{error}</p>}

Expand Down
4 changes: 2 additions & 2 deletions src/components/features/sidebar/sidebar-rail-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,15 @@ export function SidebarRailBody({
<SidebarNavLink
to="/conversations"
end
label="New Chat"
label={t(I18nKey.SIDEBAR$NEW_CHAT)}
testId="sidebar-conversations-link"
disabled={linkDisabled}
collapsed={collapsed}
icon={<Plus width={ICON_SIZE} height={ICON_SIZE} />}
/>
<SidebarNavLink
to="/customize"
label="Customize"
label={t(I18nKey.NAV$CUSTOMIZE)}
testId="sidebar-skills-link"
disabled={linkDisabled}
collapsed={collapsed}
Expand Down
2 changes: 1 addition & 1 deletion src/components/features/skills/skills-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function SkillsToolbar({
<button
type="button"
onClick={() => onSearchChange("")}
aria-label="Clear search"
aria-label={t(I18nKey.MCP$SEARCH_CLEAR)}
className="mr-2 p-1 rounded text-tertiary-alt hover:text-white cursor-pointer"
>
<X className="h-4 w-4" aria-hidden />
Expand Down
25 changes: 18 additions & 7 deletions src/hooks/chat/use-chat-attachment-upload.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";

export type ChatAttachmentUploadOptions = {
fromPaste?: boolean;
};
import { isFileImage } from "#/utils/is-file-image";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { validateFiles } from "#/utils/file-validation";
import {
formatFileValidationError,
validateFiles,
} from "#/utils/file-validation";
import { processFiles, processImages } from "#/utils/file-processing";
import { useConversationStore } from "#/stores/conversation-store";
import { I18nKey } from "#/i18n/declaration";

/**
* Shared attachment pipeline for home and conversation chat inputs.
*/
export function useChatAttachmentUpload() {
const { t } = useTranslation("openhands");
const {
images,
files,
Expand All @@ -30,7 +36,7 @@ export function useChatAttachmentUpload() {
const validation = validateFiles(selectedFiles, [...images, ...files]);

if (!validation.isValid) {
displayErrorToast(`Error: ${validation.errorMessage}`);
displayErrorToast(formatFileValidationError(validation, t));
return;
}

Expand Down Expand Up @@ -67,27 +73,32 @@ export function useChatAttachmentUpload() {
fileResults.failed.forEach(({ file, error }) => {
removeFileLoading(file.name);
displayErrorToast(
`Failed to process file ${file.name}: ${error.message}`,
t(I18nKey.CHAT_INTERFACE$FAILED_TO_PROCESS_FILE, {
name: file.name,
error: error.message,
}),
);
});

imageResults.failed.forEach(({ file, error }) => {
removeImageLoading(file.name);
displayErrorToast(
`Failed to process image ${file.name}: ${error.message}`,
t(I18nKey.CHAT_INTERFACE$FAILED_TO_PROCESS_IMAGE, {
name: file.name,
error: error.message,
}),
);
});
} catch {
validFiles.forEach((file) => removeFileLoading(file.name));
validImages.forEach((image) => removeImageLoading(image.name));
displayErrorToast(
"An unexpected error occurred while processing files",
);
displayErrorToast(t(I18nKey.CHAT_INTERFACE$FILE_PROCESSING_UNEXPECTED));
}
},
[
images,
files,
t,
addImages,
addFiles,
addFileLoading,
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/use-handle-ws-events.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { AgentState } from "#/types/agent-state";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useEventStore } from "#/stores/use-event-store";
import { useSendMessage } from "#/hooks/use-send-message";
import { I18nKey } from "#/i18n/declaration";
import {
isAgentErrorEvent,
isAgentServerEvent,
Expand All @@ -23,6 +25,7 @@ const isTypedErrorEvent = (
"type" in event && event.type === "error";

export const useHandleWSEvents = () => {
const { t } = useTranslation("openhands");
const { send } = useSendMessage();
const events = useEventStore((state) => state.events);

Expand All @@ -40,7 +43,7 @@ export const useHandleWSEvents = () => {

if (isServerError(event)) {
if (event.error_code === 401) {
displayErrorToast("Session expired.");
displayErrorToast(t(I18nKey.SESSION$EXPIRED));
return;
}

Expand All @@ -59,5 +62,5 @@ export const useHandleWSEvents = () => {
send(generateAgentStateChangeEvent(AgentState.PAUSED));
}
}
}, [events.length]);
}, [events.length, t]);
};
Loading
Loading