From dfd9e57243bc5b3d6a2ddf3a36bae174a7a205bd Mon Sep 17 00:00:00 2001
From: neubig <398875+neubig@users.noreply.github.com>
Date: Thu, 11 Jun 2026 21:07:33 -0400
Subject: [PATCH 1/2] Localize canvas text labels
---
.../automations/delete-confirmation-modal.tsx | 2 +-
.../features/backends/backend-form-modal.tsx | 4 +-
.../features/chat/chat-interface.tsx | 7 +-
.../features/chat/chat-messages-skeleton.tsx | 6 +-
.../chat/components/chat-input-actions.tsx | 2 +-
src/components/features/chat/plan-preview.tsx | 10 +-
.../conversation-tabs/conversation-tabs.tsx | 7 +-
.../features/diff-viewer/loading-spinner.tsx | 6 +-
.../git-branch-dropdown.tsx | 4 +-
.../features/home/shared/clear-button.tsx | 6 +-
.../features/home/shared/toggle-button.tsx | 6 +-
.../folder-browser-modal.tsx | 2 +-
.../features/settings/mobile-header.tsx | 2 +-
.../settings/secrets-settings/secret-form.tsx | 4 +-
.../features/sidebar/sidebar-rail-body.tsx | 4 +-
.../features/skills/skills-toolbar.tsx | 2 +-
src/hooks/chat/use-chat-attachment-upload.ts | 25 +-
src/hooks/use-handle-ws-events.ts | 7 +-
src/i18n/translation.json | 289 ++++++++++++++++++
src/ui/dropdown/clear-button.tsx | 6 +-
src/utils/file-validation.ts | 31 +-
src/utils/send-message-with-attachments.ts | 7 +-
22 files changed, 402 insertions(+), 37 deletions(-)
diff --git a/src/components/features/automations/delete-confirmation-modal.tsx b/src/components/features/automations/delete-confirmation-modal.tsx
index 6d2ea1d10..eb642ea2b 100644
--- a/src/components/features/automations/delete-confirmation-modal.tsx
+++ b/src/components/features/automations/delete-confirmation-modal.tsx
@@ -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)}
>
diff --git a/src/components/features/backends/backend-form-modal.tsx b/src/components/features/backends/backend-form-modal.tsx
index 35b1be14b..49709e4ea 100644
--- a/src/components/features/backends/backend-form-modal.tsx
+++ b/src/components/features/backends/backend-form-modal.tsx
@@ -446,7 +446,7 @@ export function BackendForm({
setConnectionError(null);
}}
onBlur={() => setNameTouched(true)}
- placeholder="Production"
+ placeholder={t(I18nKey.BACKEND$NAME_PLACEHOLDER)}
className="w-full"
showRequiredTag
error={nameError}
@@ -622,7 +622,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"
/>
diff --git a/src/components/features/chat/chat-interface.tsx b/src/components/features/chat/chat-interface.tsx
index 131c089c0..b54886a52 100644
--- a/src/components/features/chat/chat-interface.tsx
+++ b/src/components/features/chat/chat-interface.tsx
@@ -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";
@@ -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
}
diff --git a/src/components/features/chat/chat-messages-skeleton.tsx b/src/components/features/chat/chat-messages-skeleton.tsx
index 4012d68fc..dc23f8b43 100644
--- a/src/components/features/chat/chat-messages-skeleton.tsx
+++ b/src/components/features/chat/chat-messages-skeleton.tsx
@@ -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" },
@@ -22,11 +24,13 @@ function SkeletonBlock({ width, height }: { width: string; height: string }) {
}
export function ChatMessagesSkeleton() {
+ const { t } = useTranslation("openhands");
+
return (
{SKELETON_PATTERN.map((item, i) => (
diff --git a/src/components/features/chat/components/chat-input-actions.tsx b/src/components/features/chat/components/chat-input-actions.tsx
index 50392f603..1ebab199d 100644
--- a/src/components/features/chat/components/chat-input-actions.tsx
+++ b/src/components/features/chat/components/chat-input-actions.tsx
@@ -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) => {
diff --git a/src/components/features/chat/plan-preview.tsx b/src/components/features/chat/plan-preview.tsx
index 1dd63a7be..104c78c1b 100644
--- a/src/components/features/chat/plan-preview.tsx
+++ b/src/components/features/chat/plan-preview.tsx
@@ -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";
@@ -31,7 +32,6 @@ interface PlanPreviewProps {
isBuildDisabled?: boolean;
}
-/* eslint-disable i18next/no-literal-string */
export function PlanPreview({
planContent,
isStreaming,
@@ -128,11 +128,15 @@ export function PlanPreview({
: "hover:opacity-90 cursor-pointer",
)}
data-testid="plan-preview-build-button"
+ aria-label={t(I18nKey.COMMON$BUILD)}
>
{t(I18nKey.COMMON$BUILD)}{" "}
-
- ⌘↩
+
+ {BUILD_SHORTCUT_LABEL}
diff --git a/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx b/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
index 2b030251b..6839acfde 100644
--- a/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
+++ b/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
@@ -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,
@@ -366,10 +368,11 @@ export function ConversationTabs({
: "cursor-pointer hover:opacity-90",
)}
data-testid="planner-tab-build-button"
+ aria-label={t(I18nKey.COMMON$BUILD)}
>
- {/* eslint-disable-next-line i18next/no-literal-string */}
- {t(I18nKey.COMMON$BUILD)} ⌘↩
+ {t(I18nKey.COMMON$BUILD)}{" "}
+ {BUILD_SHORTCUT_LABEL}
diff --git a/src/components/features/diff-viewer/loading-spinner.tsx b/src/components/features/diff-viewer/loading-spinner.tsx
index 4b251bce4..f20e170f2 100644
--- a/src/components/features/diff-viewer/loading-spinner.tsx
+++ b/src/components/features/diff-viewer/loading-spinner.tsx
@@ -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 (
);
diff --git a/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx b/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx
index 9af809bc1..ba86b0079 100644
--- a/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx
+++ b/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx
@@ -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(() => {
diff --git a/src/components/features/home/shared/clear-button.tsx b/src/components/features/home/shared/clear-button.tsx
index 56575925e..e74cdd779 100644
--- a/src/components/features/home/shared/clear-button.tsx
+++ b/src/components/features/home/shared/clear-button.tsx
@@ -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 {
@@ -12,6 +14,8 @@ export function ClearButton({
onClear,
testId = "dropdown-clear",
}: ClearButtonProps) {
+ const { t } = useTranslation("openhands");
+
return (
diff --git a/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx b/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx
index 51368fa40..10f8af43f 100644
--- a/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx
+++ b/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx
@@ -254,7 +254,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"
>
diff --git a/src/components/features/settings/mobile-header.tsx b/src/components/features/settings/mobile-header.tsx
index d696d2344..856dbc578 100644
--- a/src/components/features/settings/mobile-header.tsx
+++ b/src/components/features/settings/mobile-header.tsx
@@ -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)}
>
{error &&
{error}
}
diff --git a/src/components/features/sidebar/sidebar-rail-body.tsx b/src/components/features/sidebar/sidebar-rail-body.tsx
index c2f60b71e..e742486d9 100644
--- a/src/components/features/sidebar/sidebar-rail-body.tsx
+++ b/src/components/features/sidebar/sidebar-rail-body.tsx
@@ -159,7 +159,7 @@ export function SidebarRailBody({
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"
>
diff --git a/src/hooks/chat/use-chat-attachment-upload.ts b/src/hooks/chat/use-chat-attachment-upload.ts
index 760b3a585..550844832 100644
--- a/src/hooks/chat/use-chat-attachment-upload.ts
+++ b/src/hooks/chat/use-chat-attachment-upload.ts
@@ -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,
@@ -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;
}
@@ -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,
diff --git a/src/hooks/use-handle-ws-events.ts b/src/hooks/use-handle-ws-events.ts
index 5b3b78ac7..f2e967e1c 100644
--- a/src/hooks/use-handle-ws-events.ts
+++ b/src/hooks/use-handle-ws-events.ts
@@ -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,
@@ -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);
@@ -40,7 +43,7 @@ export const useHandleWSEvents = () => {
if (isServerError(event)) {
if (event.error_code === 401) {
- displayErrorToast("Session expired.");
+ displayErrorToast(t(I18nKey.SESSION$EXPIRED));
return;
}
@@ -59,5 +62,5 @@ export const useHandleWSEvents = () => {
send(generateAgentStateChangeEvent(AgentState.PAUSED));
}
}
- }, [events.length]);
+ }, [events.length, t]);
};
diff --git a/src/i18n/translation.json b/src/i18n/translation.json
index bae71cfe8..71a9baa93 100644
--- a/src/i18n/translation.json
+++ b/src/i18n/translation.json
@@ -8346,6 +8346,23 @@
"uk": "Час сеансу минув!",
"ca": "La sessió ha caducat!"
},
+ "SESSION$EXPIRED": {
+ "en": "Session expired.",
+ "zh-CN": "会话已过期。",
+ "de": "Sitzung abgelaufen.",
+ "zh-TW": "會話已過期。",
+ "es": "La sesión ha caducado.",
+ "fr": "La session a expiré.",
+ "it": "Sessione scaduta.",
+ "pt": "Sessão expirada.",
+ "ko-KR": "세션이 만료되었습니다.",
+ "ar": "انتهت صلاحية الجلسة.",
+ "tr": "Oturum süresi doldu.",
+ "no": "Økten er utløpt.",
+ "ja": "セッションの有効期限が切れました。",
+ "uk": "Сеанс завершився.",
+ "ca": "La sessió ha caducat."
+ },
"EXPLORER$UPLOAD_ERROR_MESSAGE": {
"en": "Error uploading file",
"zh-CN": "上传时出错",
@@ -10777,6 +10794,108 @@
"tr": "dosya olarak yükleme",
"uk": "не завантажувати як файл"
},
+ "CHAT_INTERFACE$FILES_EXCEED_SIZE": {
+ "en": "Files exceeding {{limit}}MB are not allowed: {{files}}",
+ "ja": "{{limit}}MB を超えるファイルは許可されていません: {{files}}",
+ "zh-CN": "不允许超过 {{limit}}MB 的文件:{{files}}",
+ "zh-TW": "不允許超過 {{limit}}MB 的檔案:{{files}}",
+ "ko-KR": "{{limit}}MB를 초과하는 파일은 허용되지 않습니다: {{files}}",
+ "no": "Filer over {{limit}} MB er ikke tillatt: {{files}}",
+ "ar": "الملفات التي تتجاوز {{limit}} ميغابايت غير مسموح بها: {{files}}",
+ "de": "Dateien über {{limit}} MB sind nicht erlaubt: {{files}}",
+ "fr": "Les fichiers de plus de {{limit}} Mo ne sont pas autorisés : {{files}}",
+ "it": "I file oltre {{limit}} MB non sono consentiti: {{files}}",
+ "pt": "Arquivos acima de {{limit}} MB não são permitidos: {{files}}",
+ "es": "No se permiten archivos de más de {{limit}} MB: {{files}}",
+ "ca": "No es permeten fitxers de més de {{limit}} MB: {{files}}",
+ "tr": "{{limit}} MB üzerindeki dosyalara izin verilmez: {{files}}",
+ "uk": "Файли понад {{limit}} МБ не дозволені: {{files}}"
+ },
+ "CHAT_INTERFACE$TOTAL_FILE_SIZE_EXCEEDS_LIMIT": {
+ "en": "Total file size would be {{size}}MB, exceeding the {{limit}}MB limit. Please select fewer or smaller files.",
+ "ja": "ファイルの合計サイズは {{size}}MB になり、{{limit}}MB の制限を超えます。より少ない、または小さいファイルを選択してください。",
+ "zh-CN": "总文件大小将为 {{size}}MB,超过 {{limit}}MB 限制。请选择更少或更小的文件。",
+ "zh-TW": "檔案總大小將為 {{size}}MB,超過 {{limit}}MB 限制。請選擇較少或較小的檔案。",
+ "ko-KR": "총 파일 크기가 {{size}}MB가 되어 {{limit}}MB 제한을 초과합니다. 더 적거나 더 작은 파일을 선택하세요.",
+ "no": "Total filstørrelse vil bli {{size}} MB, som overstiger grensen på {{limit}} MB. Velg færre eller mindre filer.",
+ "ar": "سيكون إجمالي حجم الملفات {{size}} ميغابايت، متجاوزًا حد {{limit}} ميغابايت. يُرجى اختيار ملفات أقل أو أصغر.",
+ "de": "Die Gesamtgröße der Dateien wäre {{size}} MB und überschreitet das Limit von {{limit}} MB. Bitte wählen Sie weniger oder kleinere Dateien aus.",
+ "fr": "La taille totale des fichiers serait de {{size}} Mo, ce qui dépasse la limite de {{limit}} Mo. Sélectionnez moins de fichiers ou des fichiers plus petits.",
+ "it": "La dimensione totale dei file sarebbe {{size}} MB, superando il limite di {{limit}} MB. Seleziona meno file o file più piccoli.",
+ "pt": "O tamanho total dos arquivos seria {{size}} MB, excedendo o limite de {{limit}} MB. Selecione menos arquivos ou arquivos menores.",
+ "es": "El tamaño total de los archivos sería de {{size}} MB, lo que supera el límite de {{limit}} MB. Seleccione menos archivos o archivos más pequeños.",
+ "ca": "La mida total dels fitxers seria de {{size}} MB, superant el límit de {{limit}} MB. Seleccioneu menys fitxers o fitxers més petits.",
+ "tr": "Toplam dosya boyutu {{size}} MB olur ve {{limit}} MB sınırını aşar. Lütfen daha az veya daha küçük dosyalar seçin.",
+ "uk": "Загальний розмір файлів становитиме {{size}} МБ, що перевищує ліміт {{limit}} МБ. Виберіть менше або менші файли."
+ },
+ "CHAT_INTERFACE$INVALID_ATTACHMENTS": {
+ "en": "Invalid attachments",
+ "ja": "無効な添付ファイル",
+ "zh-CN": "附件无效",
+ "zh-TW": "附件無效",
+ "ko-KR": "잘못된 첨부 파일",
+ "no": "Ugyldige vedlegg",
+ "ar": "مرفقات غير صالحة",
+ "de": "Ungültige Anhänge",
+ "fr": "Pièces jointes non valides",
+ "it": "Allegati non validi",
+ "pt": "Anexos inválidos",
+ "es": "Adjuntos no válidos",
+ "ca": "Adjunts no vàlids",
+ "tr": "Geçersiz ekler",
+ "uk": "Недійсні вкладення"
+ },
+ "CHAT_INTERFACE$FAILED_TO_PROCESS_FILE": {
+ "en": "Failed to process file {{name}}: {{error}}",
+ "ja": "ファイル {{name}} の処理に失敗しました: {{error}}",
+ "zh-CN": "处理文件 {{name}} 失败:{{error}}",
+ "zh-TW": "處理檔案 {{name}} 失敗:{{error}}",
+ "ko-KR": "파일 {{name}} 처리 실패: {{error}}",
+ "no": "Kunne ikke behandle filen {{name}}: {{error}}",
+ "ar": "فشلت معالجة الملف {{name}}: {{error}}",
+ "de": "Datei {{name}} konnte nicht verarbeitet werden: {{error}}",
+ "fr": "Échec du traitement du fichier {{name}} : {{error}}",
+ "it": "Impossibile elaborare il file {{name}}: {{error}}",
+ "pt": "Falha ao processar o arquivo {{name}}: {{error}}",
+ "es": "Error al procesar el archivo {{name}}: {{error}}",
+ "ca": "No s'ha pogut processar el fitxer {{name}}: {{error}}",
+ "tr": "{{name}} dosyası işlenemedi: {{error}}",
+ "uk": "Не вдалося обробити файл {{name}}: {{error}}"
+ },
+ "CHAT_INTERFACE$FAILED_TO_PROCESS_IMAGE": {
+ "en": "Failed to process image {{name}}: {{error}}",
+ "ja": "画像 {{name}} の処理に失敗しました: {{error}}",
+ "zh-CN": "处理图片 {{name}} 失败:{{error}}",
+ "zh-TW": "處理圖片 {{name}} 失敗:{{error}}",
+ "ko-KR": "이미지 {{name}} 처리 실패: {{error}}",
+ "no": "Kunne ikke behandle bildet {{name}}: {{error}}",
+ "ar": "فشلت معالجة الصورة {{name}}: {{error}}",
+ "de": "Bild {{name}} konnte nicht verarbeitet werden: {{error}}",
+ "fr": "Échec du traitement de l'image {{name}} : {{error}}",
+ "it": "Impossibile elaborare l'immagine {{name}}: {{error}}",
+ "pt": "Falha ao processar a imagem {{name}}: {{error}}",
+ "es": "Error al procesar la imagen {{name}}: {{error}}",
+ "ca": "No s'ha pogut processar la imatge {{name}}: {{error}}",
+ "tr": "{{name}} görseli işlenemedi: {{error}}",
+ "uk": "Не вдалося обробити зображення {{name}}: {{error}}"
+ },
+ "CHAT_INTERFACE$FILE_PROCESSING_UNEXPECTED": {
+ "en": "An unexpected error occurred while processing files",
+ "ja": "ファイルの処理中に予期しないエラーが発生しました",
+ "zh-CN": "处理文件时发生意外错误",
+ "zh-TW": "處理檔案時發生非預期錯誤",
+ "ko-KR": "파일을 처리하는 동안 예기치 않은 오류가 발생했습니다",
+ "no": "Det oppstod en uventet feil under behandling av filer",
+ "ar": "حدث خطأ غير متوقع أثناء معالجة الملفات",
+ "de": "Beim Verarbeiten der Dateien ist ein unerwarteter Fehler aufgetreten",
+ "fr": "Une erreur inattendue s'est produite lors du traitement des fichiers",
+ "it": "Si è verificato un errore imprevisto durante l'elaborazione dei file",
+ "pt": "Ocorreu um erro inesperado ao processar os arquivos",
+ "es": "Se produjo un error inesperado al procesar los archivos",
+ "ca": "S'ha produït un error inesperat en processar els fitxers",
+ "tr": "Dosyalar işlenirken beklenmeyen bir hata oluştu",
+ "uk": "Під час обробки файлів сталася неочікувана помилка"
+ },
"CHAT_INTERFACE$TOOLTIP_UPLOAD_IMAGE": {
"en": "Upload image",
"zh-CN": "上传图片",
@@ -10896,6 +11015,40 @@
"uk": "Вниз",
"ca": "Ves al final"
},
+ "CHAT_INTERFACE$MORE_INPUT_ACTIONS": {
+ "en": "More input actions",
+ "zh-CN": "更多输入操作",
+ "de": "Weitere Eingabeaktionen",
+ "ko-KR": "추가 입력 작업",
+ "no": "Flere inndatahandlinger",
+ "zh-TW": "更多輸入操作",
+ "it": "Altre azioni di input",
+ "pt": "Mais ações de entrada",
+ "es": "Más acciones de entrada",
+ "ar": "مزيد من إجراءات الإدخال",
+ "fr": "Plus d'actions de saisie",
+ "tr": "Daha fazla giriş eylemi",
+ "ja": "その他の入力アクション",
+ "uk": "Більше дій введення",
+ "ca": "Més accions d'entrada"
+ },
+ "CHAT_INTERFACE$LOADING_CONVERSATION": {
+ "en": "Loading conversation",
+ "zh-CN": "正在加载对话",
+ "de": "Unterhaltung wird geladen",
+ "ko-KR": "대화 불러오는 중",
+ "no": "Laster samtale",
+ "zh-TW": "正在載入對話",
+ "it": "Caricamento conversazione",
+ "pt": "Carregando conversa",
+ "es": "Cargando conversación",
+ "ar": "جارٍ تحميل المحادثة",
+ "fr": "Chargement de la conversation",
+ "tr": "Konuşma yükleniyor",
+ "ja": "会話を読み込み中",
+ "uk": "Завантаження розмови",
+ "ca": "S'està carregant la conversa"
+ },
"CHAT_INTERFACE$MESSAGE_ARIA_LABEL": {
"en": "Message from {{sender}}",
"zh-CN": "来自 {{sender}} 的消息",
@@ -11542,6 +11695,23 @@
"tr": "İsim",
"ca": "Nom"
},
+ "SETTINGS$TOGGLE_MENU": {
+ "en": "Toggle settings menu",
+ "uk": "Перемкнути меню налаштувань",
+ "ja": "設定メニューを切り替え",
+ "zh-CN": "切换设置菜单",
+ "zh-TW": "切換設定選單",
+ "ko-KR": "설정 메뉴 전환",
+ "no": "Veksle innstillingsmeny",
+ "ar": "تبديل قائمة الإعدادات",
+ "de": "Einstellungsmenü umschalten",
+ "fr": "Afficher/masquer le menu des paramètres",
+ "it": "Attiva/disattiva menu impostazioni",
+ "pt": "Alternar menu de configurações",
+ "es": "Alternar menú de configuración",
+ "tr": "Ayarlar menüsünü aç/kapat",
+ "ca": "Commuta el menú de configuració"
+ },
"SECRETS$DESCRIPTION": {
"en": "Description",
"uk": "Опис",
@@ -14619,6 +14789,23 @@
"tr": "Yeni Sohbet",
"uk": "Нова розмова"
},
+ "SIDEBAR$NEW_CHAT": {
+ "en": "New Chat",
+ "ja": "新しいチャット",
+ "zh-CN": "新建聊天",
+ "zh-TW": "新建聊天",
+ "ko-KR": "새 채팅",
+ "no": "Ny chat",
+ "ar": "دردشة جديدة",
+ "de": "Neuer Chat",
+ "fr": "Nouveau chat",
+ "it": "Nuova chat",
+ "pt": "Novo chat",
+ "es": "Nuevo chat",
+ "ca": "Xat nou",
+ "tr": "Yeni Sohbet",
+ "uk": "Новий чат"
+ },
"STATUS$CONNECTING_TO_RUNTIME": {
"en": "Connecting to runtime...",
"zh-CN": "正在连接到运行时...",
@@ -20212,6 +20399,23 @@
"uk": "наприклад OpenAI_API_Key",
"ca": "p. ex. OpenAI_API_Key"
},
+ "SECRETS$NAME_PATTERN_HELP": {
+ "en": "Must start with a letter, contain only letters/numbers/underscores, and be 1-64 characters",
+ "ja": "文字で始まり、文字、数字、アンダースコアのみを含み、1〜64文字である必要があります",
+ "zh-CN": "必须以字母开头,只能包含字母、数字和下划线,长度为 1-64 个字符",
+ "zh-TW": "必須以字母開頭,只能包含字母、數字和底線,長度為 1-64 個字元",
+ "ko-KR": "문자로 시작해야 하며 문자, 숫자, 밑줄만 포함하고 1-64자여야 합니다",
+ "no": "Må starte med en bokstav, bare inneholde bokstaver/tall/understreker og være 1-64 tegn",
+ "it": "Deve iniziare con una lettera, contenere solo lettere/numeri/trattini bassi ed essere di 1-64 caratteri",
+ "pt": "Deve começar com uma letra, conter apenas letras/números/sublinhados e ter 1-64 caracteres",
+ "es": "Debe comenzar con una letra, contener solo letras/números/guiones bajos y tener 1-64 caracteres",
+ "ar": "يجب أن يبدأ بحرف، ويحتوي فقط على أحرف/أرقام/شرطات سفلية، وأن يكون من 1 إلى 64 حرفًا",
+ "fr": "Doit commencer par une lettre, contenir uniquement lettres/chiffres/tirets bas, et faire 1 à 64 caractères",
+ "tr": "Bir harfle başlamalı, yalnızca harf/rakam/alt çizgi içermeli ve 1-64 karakter olmalıdır",
+ "de": "Muss mit einem Buchstaben beginnen, nur Buchstaben/Zahlen/Unterstriche enthalten und 1-64 Zeichen lang sein",
+ "uk": "Має починатися з літери, містити лише літери/цифри/підкреслення та мати 1-64 символи",
+ "ca": "Ha de començar amb una lletra, contenir només lletres/números/guions baixos i tenir entre 1 i 64 caràcters"
+ },
"MODEL$CUSTOM_MODEL": {
"en": "Custom Model",
"ja": "カスタムモデル",
@@ -23289,6 +23493,57 @@
"uk": "Переглянути більше",
"ca": "Mostra més"
},
+ "COMMON$CLEAR_SELECTION": {
+ "en": "Clear selection",
+ "ja": "選択をクリア",
+ "zh-CN": "清除选择",
+ "zh-TW": "清除選取",
+ "ko-KR": "선택 지우기",
+ "no": "Fjern valg",
+ "it": "Cancella selezione",
+ "pt": "Limpar seleção",
+ "es": "Borrar selección",
+ "ar": "مسح التحديد",
+ "fr": "Effacer la sélection",
+ "tr": "Seçimi temizle",
+ "de": "Auswahl löschen",
+ "uk": "Очистити вибір",
+ "ca": "Esborra la selecció"
+ },
+ "COMMON$TOGGLE_MENU": {
+ "en": "Toggle menu",
+ "ja": "メニューを切り替え",
+ "zh-CN": "切换菜单",
+ "zh-TW": "切換選單",
+ "ko-KR": "메뉴 전환",
+ "no": "Veksle meny",
+ "it": "Attiva/disattiva menu",
+ "pt": "Alternar menu",
+ "es": "Alternar menú",
+ "ar": "تبديل القائمة",
+ "fr": "Afficher/masquer le menu",
+ "tr": "Menüyü aç/kapat",
+ "de": "Menü umschalten",
+ "uk": "Перемкнути меню",
+ "ca": "Commuta el menú"
+ },
+ "COMMON$UP": {
+ "en": "Up",
+ "ja": "上へ",
+ "zh-CN": "向上",
+ "zh-TW": "向上",
+ "ko-KR": "위로",
+ "no": "Opp",
+ "it": "Su",
+ "pt": "Para cima",
+ "es": "Arriba",
+ "ar": "أعلى",
+ "fr": "Monter",
+ "tr": "Yukarı",
+ "de": "Nach oben",
+ "uk": "Вгору",
+ "ca": "Amunt"
+ },
"COMMON$ARCHIVED": {
"en": "Archived",
"ja": "アーカイブ済み",
@@ -26893,6 +27148,40 @@
"uk": "Ім'я хоста",
"ca": "Nom del host"
},
+ "BACKEND$NAME_PLACEHOLDER": {
+ "en": "Production",
+ "ja": "本番",
+ "zh-CN": "生产环境",
+ "zh-TW": "正式環境",
+ "ko-KR": "프로덕션",
+ "no": "Produksjon",
+ "it": "Produzione",
+ "pt": "Produção",
+ "es": "Producción",
+ "ar": "الإنتاج",
+ "fr": "Production",
+ "tr": "Üretim",
+ "de": "Produktion",
+ "uk": "Продакшн",
+ "ca": "Producció"
+ },
+ "BACKEND$ADD_NAME_PLACEHOLDER": {
+ "en": "e.g. My Server",
+ "ja": "例: My Server",
+ "zh-CN": "例如 My Server",
+ "zh-TW": "例如 My Server",
+ "ko-KR": "예: My Server",
+ "no": "f.eks. My Server",
+ "it": "es. My Server",
+ "pt": "ex. My Server",
+ "es": "ej. My Server",
+ "ar": "مثل My Server",
+ "fr": "ex. My Server",
+ "tr": "örn. My Server",
+ "de": "z.B. My Server",
+ "uk": "наприклад My Server",
+ "ca": "p. ex. My Server"
+ },
"BACKEND$HOST_LABEL": {
"en": "Host",
"ja": "ホスト",
diff --git a/src/ui/dropdown/clear-button.tsx b/src/ui/dropdown/clear-button.tsx
index 98b62c05a..466057aa2 100644
--- a/src/ui/dropdown/clear-button.tsx
+++ b/src/ui/dropdown/clear-button.tsx
@@ -1,16 +1,20 @@
import { X } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
interface ClearButtonProps {
onClear: () => void;
}
export function ClearButton({ onClear }: ClearButtonProps) {
+ const { t } = useTranslation("openhands");
+
return (