From 1edb873acebb8c361412f4bb5e0d6a6f7d5e2586 Mon Sep 17 00:00:00 2001 From: lc6464 Date: Sat, 30 May 2026 18:21:40 +0800 Subject: [PATCH 1/4] feat(web): add chat image paste and drag-and-drop upload --- .../src/components/chat/chat-composer.tsx | 41 ++++- .../src/components/chat/chat-page.tsx | 166 ++++++++++------- .../src/components/chat/user-message.tsx | 2 +- web/frontend/src/features/chat/image-input.ts | 170 ++++++++++++++++++ web/frontend/src/i18n/locales/cs.json | 3 +- web/frontend/src/i18n/locales/en.json | 3 +- web/frontend/src/i18n/locales/pt-br.json | 3 +- web/frontend/src/i18n/locales/zh.json | 3 +- 8 files changed, 320 insertions(+), 71 deletions(-) create mode 100644 web/frontend/src/features/chat/image-input.ts diff --git a/web/frontend/src/components/chat/chat-composer.tsx b/web/frontend/src/components/chat/chat-composer.tsx index 43bc8a463..cae4d2f6d 100644 --- a/web/frontend/src/components/chat/chat-composer.tsx +++ b/web/frontend/src/components/chat/chat-composer.tsx @@ -1,5 +1,10 @@ import { IconArrowUp, IconPhotoPlus, IconX } from "@tabler/icons-react" -import { useRef, type KeyboardEvent as ReactKeyboardEvent } from "react" +import { + type ClipboardEvent as ReactClipboardEvent, + type DragEvent as ReactDragEvent, + type KeyboardEvent as ReactKeyboardEvent, + useRef, +} from "react" import { useTranslation } from "react-i18next" import TextareaAutosize from "react-textarea-autosize" @@ -30,11 +35,17 @@ interface ChatComposerProps { attachments: ChatAttachment[] onInputChange: (value: string) => void onAddImages: () => void + onPaste: (event: ReactClipboardEvent) => void + onDragEnter: (event: ReactDragEvent) => void + onDragLeave: (event: ReactDragEvent) => void + onDragOver: (event: ReactDragEvent) => void + onDrop: (event: ReactDragEvent) => void onRemoveAttachment: (index: number) => void onSend: () => void onContextDetail?: () => void inputDisabledReason: ChatInputDisabledReason | null canSend: boolean + isDragActive: boolean contextUsage?: ContextUsage } @@ -43,11 +54,17 @@ export function ChatComposer({ attachments, onInputChange, onAddImages, + onPaste, + onDragEnter, + onDragLeave, + onDragOver, + onDrop, onRemoveAttachment, onSend, onContextDetail, inputDisabledReason, canSend, + isDragActive, contextUsage, }: ChatComposerProps) { const { t } = useTranslation() @@ -78,8 +95,25 @@ export function ChatComposer({ } return ( -
-
+
+
+ {isDragActive && ( +
+
+ {t("chat.dropImagesActive")} +
+
+ )} + {attachments.length > 0 && (
{attachments.map((attachment, index) => ( @@ -115,6 +149,7 @@ export function ChatComposer({ onCompositionEnd={() => { composingRef.current = false }} + onPaste={onPaste} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={!canInput} diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx index 3b158843c..98766bfd5 100644 --- a/web/frontend/src/components/chat/chat-page.tsx +++ b/web/frontend/src/components/chat/chat-page.tsx @@ -1,8 +1,14 @@ import { IconPlus } from "@tabler/icons-react" import { useAtom } from "jotai" -import { type ChangeEvent, useEffect, useRef, useState } from "react" +import { + type ChangeEvent, + type ClipboardEvent, + type DragEvent, + useEffect, + useRef, + useState, +} from "react" import { useTranslation } from "react-i18next" -import { toast } from "sonner" import { AssistantMessage } from "@/components/chat/assistant-message" import { @@ -23,6 +29,12 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { + CHAT_IMAGE_ACCEPT, + buildChatImageAttachments, + getTransferredFiles, + hasFileTransfer, +} from "@/features/chat/image-input" import { useChatModels } from "@/hooks/use-chat-models" import { useGateway } from "@/hooks/use-gateway" import { usePicoChat } from "@/hooks/use-pico-chat" @@ -36,32 +48,6 @@ import { } from "@/store/chat" import type { GatewayState } from "@/store/gateway" -const MAX_IMAGE_SIZE_BYTES = 7 * 1024 * 1024 -const MAX_IMAGE_SIZE_LABEL = "7 MB" -const ALLOWED_IMAGE_TYPES = new Set([ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/bmp", -]) - -function readFileAsDataUrl(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => { - if (typeof reader.result === "string") { - resolve(reader.result) - return - } - reject(new Error("Failed to read file")) - } - reader.onerror = () => - reject(reader.error || new Error("Failed to read file")) - reader.readAsDataURL(file) - }) -} - function resolveChatInputDisabledReason({ hasDefaultModel, connectionState, @@ -118,10 +104,12 @@ export function ChatPage() { const { t } = useTranslation() const scrollRef = useRef(null) const fileInputRef = useRef(null) + const dragDepthRef = useRef(0) const [isAtBottom, setIsAtBottom] = useState(true) const [hasScrolled, setHasScrolled] = useState(false) const [input, setInput] = useState("") const [attachments, setAttachments] = useState([]) + const [isDragActive, setIsDragActive] = useState(false) const [assistantDetailVisibility, setAssistantDetailVisibility] = useAtom( assistantDetailVisibilityAtom, ) @@ -223,6 +211,19 @@ export function ChatPage() { setAttachments((prev) => prev.filter((_, itemIndex) => itemIndex !== index)) } + const appendImageFiles = async (files: readonly File[]) => { + if (!canInput || files.length === 0) { + return + } + + const nextAttachments = await buildChatImageAttachments(files, t) + if (nextAttachments.length === 0) { + return + } + + setAttachments((prev) => [...prev, ...nextAttachments]) + } + const handleImageSelection = async (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []) event.target.value = "" @@ -231,45 +232,77 @@ export function ChatPage() { return } - const nextAttachments: ChatAttachment[] = [] - for (const file of files) { - if (!ALLOWED_IMAGE_TYPES.has(file.type)) { - toast.error( - t("chat.invalidImage", { - name: file.name, - }), - ) - continue - } + await appendImageFiles(files) + } - if (file.size > MAX_IMAGE_SIZE_BYTES) { - toast.error( - t("chat.imageTooLarge", { - name: file.name, - size: MAX_IMAGE_SIZE_LABEL, - }), - ) - continue - } + const resetDragState = () => { + dragDepthRef.current = 0 + setIsDragActive(false) + } - try { - nextAttachments.push({ - type: "image", - filename: file.name, - url: await readFileAsDataUrl(file), - }) - } catch { - toast.error( - t("chat.imageReadFailed", { - name: file.name, - }), - ) - } + const handleComposerPaste = async ( + event: ClipboardEvent, + ) => { + const files = getTransferredFiles(event.clipboardData) + if (files.length === 0) { + return } - if (nextAttachments.length > 0) { - setAttachments(nextAttachments.slice(0, 1)) + await appendImageFiles(files) + } + + const handleComposerDragEnter = (event: DragEvent) => { + if (!hasFileTransfer(event.dataTransfer)) { + return } + + event.preventDefault() + if (!canInput) { + return + } + dragDepthRef.current += 1 + setIsDragActive(true) + } + + const handleComposerDragLeave = (event: DragEvent) => { + if (!hasFileTransfer(event.dataTransfer)) { + return + } + + event.preventDefault() + if (!canInput) { + resetDragState() + return + } + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1) + if (dragDepthRef.current === 0) { + setIsDragActive(false) + } + } + + const handleComposerDragOver = (event: DragEvent) => { + if (!hasFileTransfer(event.dataTransfer)) { + return + } + + event.preventDefault() + event.dataTransfer.dropEffect = canInput ? "copy" : "none" + } + + const handleComposerDrop = async (event: DragEvent) => { + if (!hasFileTransfer(event.dataTransfer)) { + return + } + + event.preventDefault() + const files = getTransferredFiles(event.dataTransfer) + resetDragState() + + if (!canInput || files.length === 0) { + return + } + + await appendImageFiles(files) } const canSubmit = @@ -398,7 +431,8 @@ export function ChatPage() { @@ -408,6 +442,11 @@ export function ChatPage() { attachments={attachments} onInputChange={setInput} onAddImages={handleAddImages} + onPaste={handleComposerPaste} + onDragEnter={handleComposerDragEnter} + onDragLeave={handleComposerDragLeave} + onDragOver={handleComposerDragOver} + onDrop={handleComposerDrop} onRemoveAttachment={handleRemoveAttachment} onSend={handleSend} onContextDetail={() => { @@ -417,6 +456,7 @@ export function ChatPage() { }} inputDisabledReason={inputDisabledReason} canSend={canSubmit} + isDragActive={isDragActive} contextUsage={contextUsage} />
diff --git a/web/frontend/src/components/chat/user-message.tsx b/web/frontend/src/components/chat/user-message.tsx index 80ebc7ca0..9a05c961d 100644 --- a/web/frontend/src/components/chat/user-message.tsx +++ b/web/frontend/src/components/chat/user-message.tsx @@ -39,7 +39,7 @@ export function UserMessage({ {attachment.filename ))} diff --git a/web/frontend/src/features/chat/image-input.ts b/web/frontend/src/features/chat/image-input.ts new file mode 100644 index 000000000..ed424f084 --- /dev/null +++ b/web/frontend/src/features/chat/image-input.ts @@ -0,0 +1,170 @@ +import type { TFunction } from "i18next" +import { toast } from "sonner" + +import type { ChatAttachment } from "@/store/chat" + +const CHAT_IMAGE_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/bmp", +] as const + +const CHAT_IMAGE_MIME_TYPE_SET = new Set(CHAT_IMAGE_MIME_TYPES) +const CHAT_IMAGE_EXTENSION_BY_MIME: Record = { + "image/jpeg": ".jpg", + "image/png": ".png", + "image/gif": ".gif", + "image/webp": ".webp", + "image/bmp": ".bmp", +} +const CHAT_IMAGE_MIME_BY_EXTENSION: Record = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", + ".webp": "image/webp", + ".bmp": "image/bmp", +} + +export const CHAT_IMAGE_ACCEPT = CHAT_IMAGE_MIME_TYPES.join(",") + +const MAX_CHAT_IMAGE_SIZE_BYTES = 7 * 1024 * 1024 +const MAX_CHAT_IMAGE_SIZE_LABEL = "7 MB" + +function readFileAsDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result) + return + } + reject(new Error("Failed to read file")) + } + reader.onerror = () => + reject(reader.error || new Error("Failed to read file")) + reader.readAsDataURL(file) + }) +} + +function getFileExtension(fileName: string): string { + const lastDotIndex = fileName.lastIndexOf(".") + if (lastDotIndex === -1) { + return "" + } + return fileName.slice(lastDotIndex).toLowerCase() +} + +function getSupportedImageMimeType(file: File): string | null { + const normalizedType = file.type.trim().toLowerCase() + if (normalizedType && CHAT_IMAGE_MIME_TYPE_SET.has(normalizedType)) { + return normalizedType + } + + const extension = getFileExtension(file.name) + return CHAT_IMAGE_MIME_BY_EXTENSION[extension] ?? null +} + +function normalizeImageFileForDataUrl(file: File, filename: string): File { + const mimeType = getSupportedImageMimeType(file) + if (!mimeType || file.type.trim().toLowerCase() === mimeType) { + return file + } + + const normalizedName = file.name.trim() || filename + return new File([file], normalizedName, { type: mimeType }) +} + +function getAttachmentFilename(file: File, index: number): string { + const trimmedName = file.name.trim() + if (trimmedName) { + return trimmedName + } + + const mimeType = getSupportedImageMimeType(file) + const extension = mimeType ? CHAT_IMAGE_EXTENSION_BY_MIME[mimeType] : ".png" + return `image-${index + 1}${extension}` +} + +function getTransferItemFiles(dataTransfer: DataTransfer | null): File[] { + if (!dataTransfer) { + return [] + } + + const files = Array.from(dataTransfer.files) + if (files.length > 0) { + return files + } + + return Array.from(dataTransfer.items) + .filter((item) => item.kind === "file") + .map((item) => item.getAsFile()) + .filter((file): file is File => file !== null) +} + +export function hasFileTransfer(dataTransfer: DataTransfer | null): boolean { + if (!dataTransfer) { + return false + } + + if (dataTransfer.files.length > 0) { + return true + } + + return Array.from(dataTransfer.items).some((item) => item.kind === "file") +} + +export function getTransferredFiles(dataTransfer: DataTransfer | null) { + return getTransferItemFiles(dataTransfer) +} + +export async function buildChatImageAttachments( + files: readonly File[], + t: TFunction, +): Promise { + const nextAttachments: ChatAttachment[] = [] + + for (const [index, file] of files.entries()) { + const filename = getAttachmentFilename(file, index) + + const mimeType = getSupportedImageMimeType(file) + if (!mimeType) { + toast.error( + t("chat.invalidImage", { + name: filename, + }), + ) + continue + } + + if (file.size > MAX_CHAT_IMAGE_SIZE_BYTES) { + toast.error( + t("chat.imageTooLarge", { + name: filename, + size: MAX_CHAT_IMAGE_SIZE_LABEL, + }), + ) + continue + } + + try { + const normalizedFile = normalizeImageFileForDataUrl(file, filename) + nextAttachments.push({ + type: "image", + filename, + url: await readFileAsDataUrl(normalizedFile), + contentType: mimeType, + }) + } catch { + toast.error( + t("chat.imageReadFailed", { + name: filename, + }), + ) + } + } + + return nextAttachments +} diff --git a/web/frontend/src/i18n/locales/cs.json b/web/frontend/src/i18n/locales/cs.json index 8f3fa9a25..30139c0b9 100644 --- a/web/frontend/src/i18n/locales/cs.json +++ b/web/frontend/src/i18n/locales/cs.json @@ -95,8 +95,9 @@ "contextTitle": "Kontext", "contextDetail": "Zobrazit detail", "attachImage": "Přidat obrázky", + "dropImagesActive": "Uvolněním přidáte obrázky", "removeImage": "Odebrat obrázek", - "uploadedImage": "Nahraný obrázek", + "uploadedImage": "Přiložený obrázek", "invalidImage": "\"{{name}}\" není podporovaný formát obrázku.", "imageTooLarge": "\"{{name}}\" překračuje limit {{size}}.", "imageReadFailed": "Čtení souboru \"{{name}}\" selhalo.", diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index cb97f0a5e..11ea2cabf 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -97,8 +97,9 @@ "contextTitle": "Context", "contextDetail": "View Details", "attachImage": "Add images", + "dropImagesActive": "Release to add images", "removeImage": "Remove image", - "uploadedImage": "Uploaded image", + "uploadedImage": "Attached image", "invalidImage": "\"{{name}}\" is not a supported image file.", "imageTooLarge": "\"{{name}}\" exceeds the {{size}} limit.", "imageReadFailed": "Failed to read \"{{name}}\".", diff --git a/web/frontend/src/i18n/locales/pt-br.json b/web/frontend/src/i18n/locales/pt-br.json index ca1f9ed32..28df583ba 100644 --- a/web/frontend/src/i18n/locales/pt-br.json +++ b/web/frontend/src/i18n/locales/pt-br.json @@ -97,8 +97,9 @@ "contextTitle": "Contexto", "contextDetail": "Ver Detalhes", "attachImage": "Adicionar imagens", + "dropImagesActive": "Solte para adicionar imagens", "removeImage": "Remover imagem", - "uploadedImage": "Imagem enviada", + "uploadedImage": "Imagem anexada", "invalidImage": "\"{{name}}\" não é um arquivo de imagem suportado.", "imageTooLarge": "\"{{name}}\" excede o limite de {{size}}.", "imageReadFailed": "Falha ao ler \"{{name}}\".", diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index 5590adab2..d5491e8f0 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -97,8 +97,9 @@ "contextTitle": "上下文", "contextDetail": "查看详情", "attachImage": "添加图片", + "dropImagesActive": "松开以添加图片", "removeImage": "移除图片", - "uploadedImage": "已上传图片", + "uploadedImage": "已添加图片", "invalidImage": "“{{name}}”不是支持的图片文件。", "imageTooLarge": "“{{name}}”超过了 {{size}} 限制。", "imageReadFailed": "读取“{{name}}”失败。", From 995005a0bad9be18a133e7cf93fc6cced7f3ac51 Mon Sep 17 00:00:00 2001 From: Kunal Karmakar Date: Sat, 30 May 2026 13:40:29 +0000 Subject: [PATCH 2/4] Add azure entra id support for azure openai provider --- README.md | 4 +- go.mod | 7 ++ go.sum | 19 ++++++ pkg/providers/azure/identity.go | 55 +++++++++++++++ pkg/providers/azure/identity_stub.go | 23 +++++++ pkg/providers/azure/identity_test.go | 39 +++++++++++ pkg/providers/azure/provider.go | 50 ++++++++++++-- pkg/providers/azure/provider_test.go | 67 +++++++++++++++++++ pkg/providers/factory_provider.go | 25 ++++--- .../factory_provider_azidentity_test.go | 36 ++++++++++ pkg/providers/factory_provider_test.go | 7 +- 11 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 pkg/providers/azure/identity.go create mode 100644 pkg/providers/azure/identity_stub.go create mode 100644 pkg/providers/azure/identity_test.go create mode 100644 pkg/providers/factory_provider_azidentity_test.go diff --git a/README.md b/README.md index 2fa71230d..47191ec06 100644 --- a/README.md +++ b/README.md @@ -413,12 +413,14 @@ PicoClaw supports 30+ LLM providers through the `model_list` configuration. Use | [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted | | [vLLM](https://docs.vllm.ai/) | `vllm/` | Not needed | Local deployment, OpenAI-compatible | | [LiteLLM](https://docs.litellm.ai/) | `litellm/` | Varies | Proxy for 100+ providers | -| [Azure OpenAI](https://portal.azure.com/) | `azure/` | Required | Enterprise Azure deployment | +| [Azure OpenAI](https://portal.azure.com/) | `azure/` | API key or Entra ID** | Enterprise Azure deployment | | [GitHub Copilot](https://github.com/features/copilot) | `github-copilot/` | OAuth | Device code login | | [Antigravity](https://console.cloud.google.com/) | `antigravity/` | OAuth | Google Cloud AI | | [AWS Bedrock](https://console.aws.amazon.com/bedrock)* | `bedrock/` | AWS credentials | Claude, Llama, Mistral on AWS | > \* AWS Bedrock requires build tag: `go build -tags bedrock`. Set `api_base` to a region name (e.g., `us-east-1`) for automatic endpoint resolution across all AWS partitions (aws, aws-cn, aws-us-gov). When using a full endpoint URL instead, you must also configure `AWS_REGION` via environment variable or AWS config/profile. +> +> \*\* Azure OpenAI uses `api_key` when set. If `api_key` is omitted, the provider falls back to Microsoft Entra ID via `DefaultAzureCredential` (env vars, workload identity, managed identity, Azure CLI, etc.). The Entra ID path requires build tag: `go build -tags azidentity`.
Local deployment (Ollama, vLLM, etc.) diff --git a/go.mod b/go.mod index 89e25c598..a02252114 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.25.10 require ( fyne.io/systray v1.12.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/SevereCloud/vksdk/v3 v3.3.1 github.com/adhocore/gronx v1.20.0 github.com/anthropics/anthropic-sdk-go v1.26.0 @@ -55,6 +57,8 @@ require ( require ( aead.dev/minisign v0.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect @@ -82,7 +86,9 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -91,6 +97,7 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect github.com/pion/randutil v0.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/segmentio/asm v1.1.3 // indirect diff --git a/go.sum b/go.sum index d680a6f08..167daac83 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,18 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ= fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg= @@ -164,6 +176,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= @@ -179,6 +193,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/larksuite/oapi-sdk-go/v3 v3.7.5 h1:dimv+ZAGia01f4xCDGvCiBHKWMf4K1AB7fGsM+lv5Jw= github.com/larksuite/oapi-sdk-go/v3 v3.7.5/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/line/line-bot-sdk-go/v8 v8.20.0 h1:Jv22DV3JuQ5qZvniqUbg504bJrVzffXs2CMpyoiuIZU= @@ -225,6 +241,8 @@ github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo= github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= github.com/pion/webrtc/v3 v3.3.6 h1:7XAh4RPtlY1Vul6/GmZrv7z+NnxKA6If0KStXBI2ZLE= github.com/pion/webrtc/v3 v3.3.6/go.mod h1:zyN7th4mZpV27eXybfR/cnUf3J2DRy8zw/mdjD9JTNM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -384,6 +402,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/providers/azure/identity.go b/pkg/providers/azure/identity.go new file mode 100644 index 000000000..067573a54 --- /dev/null +++ b/pkg/providers/azure/identity.go @@ -0,0 +1,55 @@ +//go:build azidentity + +// Package azure: Entra ID (DefaultAzureCredential) auth adapter. +// Built only when -tags azidentity is supplied; otherwise identity_stub.go +// satisfies the same exported API with a friendly error. +package azure + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" +) + +// azureOpenAIScope is the OAuth scope for Azure OpenAI (Cognitive Services). +// Service-wide scope, so it covers all regions including sovereign clouds. +const azureOpenAIScope = "https://cognitiveservices.azure.com/.default" + +// NewProviderWithIdentity creates an Azure OpenAI provider authenticated via +// the DefaultAzureCredential chain (env vars, workload identity, managed +// identity, Azure CLI, ...). Construction itself only fails if the credential +// chain cannot be built; misconfigured environments surface their error on +// the first Chat call when GetToken is invoked. +func NewProviderWithIdentity(apiBase, proxy, userAgent string, opts ...Option) (*Provider, error) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, fmt.Errorf("creating azure default credential: %w", err) + } + + ts := func(ctx context.Context) (string, error) { + tok, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{azureOpenAIScope}, + }) + if err != nil { + return "", fmt.Errorf("acquiring azure access token: %w", err) + } + return tok.Token, nil + } + + return NewProviderWithTokenSource(apiBase, proxy, userAgent, ts, opts...), nil +} + +// NewProviderWithIdentityAndTimeout mirrors NewProviderWithTimeout for the +// identity auth path. +func NewProviderWithIdentityAndTimeout( + apiBase, proxy, userAgent string, + requestTimeoutSeconds int, +) (*Provider, error) { + return NewProviderWithIdentity( + apiBase, proxy, userAgent, + WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second), + ) +} diff --git a/pkg/providers/azure/identity_stub.go b/pkg/providers/azure/identity_stub.go new file mode 100644 index 000000000..3fec89e26 --- /dev/null +++ b/pkg/providers/azure/identity_stub.go @@ -0,0 +1,23 @@ +//go:build !azidentity + +// Package azure: stub for the Entra ID auth path when built without +// the azidentity tag. Mirrors the exported surface of identity.go so +// callers compile cleanly in the default build. +package azure + +import "fmt" + +const azidentityBuildHint = "azure identity auth not available: build with -tags azidentity to enable Entra ID auth, or set api_key" + +// NewProviderWithIdentity returns an error in the default build. +func NewProviderWithIdentity(apiBase, proxy, userAgent string, opts ...Option) (*Provider, error) { + return nil, fmt.Errorf("%s", azidentityBuildHint) +} + +// NewProviderWithIdentityAndTimeout returns an error in the default build. +func NewProviderWithIdentityAndTimeout( + apiBase, proxy, userAgent string, + requestTimeoutSeconds int, +) (*Provider, error) { + return nil, fmt.Errorf("%s", azidentityBuildHint) +} diff --git a/pkg/providers/azure/identity_test.go b/pkg/providers/azure/identity_test.go new file mode 100644 index 000000000..e40fd1ce1 --- /dev/null +++ b/pkg/providers/azure/identity_test.go @@ -0,0 +1,39 @@ +//go:build azidentity + +package azure + +import ( + "testing" +) + +func TestNewProviderWithIdentity_Construction(t *testing.T) { + // DefaultAzureCredential construction itself does not require any env vars; + // failures surface only on the first GetToken call. Verify we get a + // non-nil provider back with a token source wired in. + p, err := NewProviderWithIdentity("https://example.openai.azure.com", "", "ua-test") + if err != nil { + t.Fatalf("NewProviderWithIdentity() error = %v", err) + } + if p == nil { + t.Fatal("NewProviderWithIdentity() returned nil provider") + } + if p.tokenSource == nil { + t.Fatal("provider.tokenSource should be set") + } + if p.apiKey != "" { + t.Errorf("provider.apiKey = %q, want empty", p.apiKey) + } +} + +func TestNewProviderWithIdentityAndTimeout_Construction(t *testing.T) { + p, err := NewProviderWithIdentityAndTimeout("https://example.openai.azure.com", "", "ua-test", 30) + if err != nil { + t.Fatalf("NewProviderWithIdentityAndTimeout() error = %v", err) + } + if p == nil { + t.Fatal("returned nil provider") + } + if p.httpClient.Timeout.Seconds() != 30 { + t.Errorf("timeout = %v, want 30s", p.httpClient.Timeout) + } +} diff --git a/pkg/providers/azure/provider.go b/pkg/providers/azure/provider.go index 7de703248..fdbbf4a30 100644 --- a/pkg/providers/azure/provider.go +++ b/pkg/providers/azure/provider.go @@ -33,10 +33,11 @@ const ( // It handles Azure-specific authentication (Bearer token), URL construction // (Responses API), and request/response formatting. type Provider struct { - apiKey string - apiBase string - httpClient *http.Client - userAgent string + apiKey string + apiBase string + httpClient *http.Client + userAgent string + tokenSource func(ctx context.Context) (string, error) } // Option configures the Azure Provider. @@ -58,6 +59,14 @@ func WithUserAgent(userAgent string) Option { } } +// WithTokenSource sets a callback that returns a bearer token per request. +// When set, it takes precedence over the static api key. +func WithTokenSource(ts func(ctx context.Context) (string, error)) Option { + return func(p *Provider) { + p.tokenSource = ts + } +} + // NewProvider creates a new Azure OpenAI provider. func NewProvider(apiKey, apiBase, proxy, userAgent string, opts ...Option) *Provider { p := &Provider{ @@ -84,6 +93,30 @@ func NewProviderWithTimeout(apiKey, apiBase, proxy, userAgent string, requestTim ) } +// NewProviderWithTokenSource creates a new Azure OpenAI provider that obtains its +// bearer token from the supplied callback on every request. Used for Entra ID auth +// where tokens are short-lived and refreshed by the underlying credential. +func NewProviderWithTokenSource( + apiBase, proxy, userAgent string, + tokenSource func(ctx context.Context) (string, error), + opts ...Option, +) *Provider { + p := &Provider{ + apiBase: strings.TrimRight(apiBase, "/"), + userAgent: userAgent, + httpClient: common.NewHTTPClient(proxy), + tokenSource: tokenSource, + } + + for _, opt := range opts { + if opt != nil { + opt(p) + } + } + + return p +} + // Chat sends a request to the Azure OpenAI Responses API endpoint. // The model parameter is passed in the request body. func (p *Provider) Chat( @@ -147,7 +180,14 @@ func (p *Provider) Chat( } req.Header.Set("Content-Type", "application/json") - if p.apiKey != "" { + switch { + case p.tokenSource != nil: + tok, tokErr := p.tokenSource(ctx) + if tokErr != nil { + return nil, fmt.Errorf("acquiring azure identity token: %w", tokErr) + } + req.Header.Set("Authorization", "Bearer "+tok) + case p.apiKey != "": req.Header.Set("Authorization", "Bearer "+p.apiKey) } if p.userAgent != "" { diff --git a/pkg/providers/azure/provider_test.go b/pkg/providers/azure/provider_test.go index 816ae97dc..b39c956fc 100644 --- a/pkg/providers/azure/provider_test.go +++ b/pkg/providers/azure/provider_test.go @@ -1,7 +1,9 @@ package azure import ( + "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "strings" @@ -415,3 +417,68 @@ func TestProviderChat_AzureNoNativeWebSearch(t *testing.T) { t.Errorf("tool type = %v, want %q", tool["type"], "function") } } + +func TestProviderChat_AzureTokenSourceHeader(t *testing.T) { + var capturedAuth string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedAuth = r.Header.Get("Authorization") + writeValidResponse(w) + })) + defer server.Close() + + ts := func(ctx context.Context) (string, error) { + return "fake-entra-token", nil + } + p := NewProviderWithTokenSource(server.URL, "", "", ts) + _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + if capturedAuth != "Bearer fake-entra-token" { + t.Errorf("Authorization header = %q, want %q", capturedAuth, "Bearer fake-entra-token") + } +} + +func TestProviderChat_AzureTokenSourceError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeValidResponse(w) + })) + defer server.Close() + + wantErr := errors.New("creds gone") + ts := func(ctx context.Context) (string, error) { + return "", wantErr + } + p := NewProviderWithTokenSource(server.URL, "", "", ts) + _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil) + if err == nil { + t.Fatal("expected error from token source") + } + if !strings.Contains(err.Error(), "creds gone") { + t.Errorf("error %q should wrap original token source error", err.Error()) + } +} + +func TestProviderChat_AzureTokenSourcePrecedence(t *testing.T) { + var capturedAuth string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedAuth = r.Header.Get("Authorization") + writeValidResponse(w) + })) + defer server.Close() + + ts := func(ctx context.Context) (string, error) { + return "from-token-source", nil + } + // Provider with both an api_key AND a token source: token source must win. + p := NewProvider("static-api-key", server.URL, "", "", WithTokenSource(ts)) + _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + if capturedAuth != "Bearer from-token-source" { + t.Errorf("Authorization header = %q, want token-source value", capturedAuth) + } +} diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 7c03daaba..5f0bfe88d 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -137,23 +137,32 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err return finalizeProviderFromConfig(provider, modelID, cfg) case "azure": - // Azure OpenAI uses deployment-based URLs, api-key header auth, - // and always sends max_completion_tokens. - if cfg.APIKey() == "" { - return nil, "", fmt.Errorf("api_key is required for azure protocol") - } + // Azure OpenAI uses deployment-based URLs. Auth is Bearer token via api_key + // when set; otherwise falls back to Entra ID (DefaultAzureCredential). if cfg.APIBase == "" { return nil, "", fmt.Errorf( "api_base is required for azure protocol (e.g., https://your-resource.openai.azure.com)", ) } - return finalizeProviderFromConfig(azure.NewProviderWithTimeout( - cfg.APIKey(), + if cfg.APIKey() != "" { + return finalizeProviderFromConfig(azure.NewProviderWithTimeout( + cfg.APIKey(), + cfg.APIBase, + cfg.Proxy, + userAgent, + cfg.RequestTimeout, + ), modelID, cfg) + } + provider, err := azure.NewProviderWithIdentityAndTimeout( cfg.APIBase, cfg.Proxy, userAgent, cfg.RequestTimeout, - ), modelID, cfg) + ) + if err != nil { + return nil, "", err + } + return finalizeProviderFromConfig(provider, modelID, cfg) case "bedrock": // AWS Bedrock uses AWS SDK credentials (env vars, profiles, IAM roles, etc.) diff --git a/pkg/providers/factory_provider_azidentity_test.go b/pkg/providers/factory_provider_azidentity_test.go new file mode 100644 index 000000000..98dfc211e --- /dev/null +++ b/pkg/providers/factory_provider_azidentity_test.go @@ -0,0 +1,36 @@ +//go:build azidentity + +// PicoClaw - Ultra-lightweight personal AI agent +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package providers + +import ( + "testing" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// With the azidentity build tag, an azure config with no api_key must succeed +// (falls back to DefaultAzureCredential). Construction does not require any +// real Azure environment — token acquisition happens on first Chat. +func TestCreateProviderFromConfig_AzureIdentityFallback(t *testing.T) { + cfg := &config.ModelConfig{ + ModelName: "azure-gpt5", + Model: "azure/my-gpt5-deployment", + APIBase: "https://my-resource.openai.azure.com", + } + + provider, modelID, err := CreateProviderFromConfig(cfg) + if err != nil { + t.Fatalf("CreateProviderFromConfig() error = %v", err) + } + if provider == nil { + t.Fatal("CreateProviderFromConfig() returned nil provider") + } + if modelID != "my-gpt5-deployment" { + t.Errorf("modelID = %q, want %q", modelID, "my-gpt5-deployment") + } +} diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index 0b3dd791b..c4ef8a4aa 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -870,8 +870,11 @@ func TestCreateProviderFromConfig_AzureMissingAPIKey(t *testing.T) { } _, _, err := CreateProviderFromConfig(cfg) - if err == nil { - t.Fatal("CreateProviderFromConfig() expected error for missing API key") + // Without api_key the factory falls back to identity auth, which in the + // default build is stubbed out and surfaces a build-tag error. With the + // azidentity tag, the call succeeds and is covered by a separate test. + if err != nil && !strings.Contains(err.Error(), "azidentity") { + t.Fatalf("CreateProviderFromConfig() unexpected error = %v", err) } } From 46e5b59d5fdaec35dfae472b75218263671a2b4b Mon Sep 17 00:00:00 2001 From: Kunal Karmakar Date: Sat, 30 May 2026 14:11:39 +0000 Subject: [PATCH 3/4] Fix linting --- pkg/providers/azure/identity.go | 3 ++- pkg/providers/azure/identity_stub.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/providers/azure/identity.go b/pkg/providers/azure/identity.go index 067573a54..1283cb070 100644 --- a/pkg/providers/azure/identity.go +++ b/pkg/providers/azure/identity.go @@ -1,8 +1,9 @@ //go:build azidentity -// Package azure: Entra ID (DefaultAzureCredential) auth adapter. +// Entra ID (DefaultAzureCredential) auth adapter. // Built only when -tags azidentity is supplied; otherwise identity_stub.go // satisfies the same exported API with a friendly error. + package azure import ( diff --git a/pkg/providers/azure/identity_stub.go b/pkg/providers/azure/identity_stub.go index 3fec89e26..c6d5a60d0 100644 --- a/pkg/providers/azure/identity_stub.go +++ b/pkg/providers/azure/identity_stub.go @@ -1,8 +1,9 @@ //go:build !azidentity -// Package azure: stub for the Entra ID auth path when built without -// the azidentity tag. Mirrors the exported surface of identity.go so -// callers compile cleanly in the default build. +// Stub for the Entra ID auth path when built without the azidentity tag. +// Mirrors the exported surface of identity.go so callers compile cleanly +// in the default build. + package azure import "fmt" From 2391f32fc182a29d80c9e21d58b6c5fb42737c8e Mon Sep 17 00:00:00 2001 From: Kunal Karmakar Date: Sat, 30 May 2026 14:52:54 +0000 Subject: [PATCH 4/4] Add Bangla support bn-in --- web/frontend/src/components/app-header.tsx | 3 + web/frontend/src/i18n/index.ts | 7 + web/frontend/src/i18n/locales/bn-in.json | 960 +++++++++++++++++++++ 3 files changed, 970 insertions(+) create mode 100644 web/frontend/src/i18n/locales/bn-in.json diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx index 30c05ee45..eb90881fb 100644 --- a/web/frontend/src/components/app-header.tsx +++ b/web/frontend/src/components/app-header.tsx @@ -291,6 +291,9 @@ export function AppHeader() { i18n.changeLanguage("pt-BR")}> Português (Brasil) + i18n.changeLanguage("bn-IN")}> + বাংলা + i18n.changeLanguage("zh")}> 简体中文 diff --git a/web/frontend/src/i18n/index.ts b/web/frontend/src/i18n/index.ts index 8c0248915..7eefd943f 100644 --- a/web/frontend/src/i18n/index.ts +++ b/web/frontend/src/i18n/index.ts @@ -1,4 +1,5 @@ import dayjs from "dayjs" +import "dayjs/locale/bn" import "dayjs/locale/cs" import "dayjs/locale/en" import "dayjs/locale/pt-br" @@ -11,6 +12,7 @@ import { initReactI18next } from "react-i18next" import en from "./locales/en.json" import ptBr from "./locales/pt-br.json" +import bnIn from "./locales/bn-in.json" import zh from "./locales/zh.json" import cs from "./locales/cs.json" @@ -33,6 +35,9 @@ i18n "pt-BR": { translation: ptBr, }, + "bn-IN": { + translation: bnIn, + }, zh: { translation: zh, }, @@ -53,6 +58,8 @@ i18n.on("languageChanged", (lng) => { dayjs.locale("zh-cn") } else if (lng.startsWith("pt")) { dayjs.locale("pt-br") + } else if (lng.startsWith("bn")) { + dayjs.locale("bn") } else if (lng.startsWith("cs")) { dayjs.locale("cs") } else { diff --git a/web/frontend/src/i18n/locales/bn-in.json b/web/frontend/src/i18n/locales/bn-in.json new file mode 100644 index 000000000..c9a7249bf --- /dev/null +++ b/web/frontend/src/i18n/locales/bn-in.json @@ -0,0 +1,960 @@ +{ + "navigation": { + "chat": "চ্যাট", + "model_group": "মডেল", + "models": "মডেল", + "credentials": "ক্রেডেনশিয়াল", + "agent_group": "এজেন্ট", + "hub": "Hub", + "skills": "দক্ষতা", + "tools": "সরঞ্জাম", + "services": "সার্ভিস", + "channels_group": "চ্যানেল", + "show_more_channels": "আরও", + "show_less_channels": "কম", + "config": "কনফিগ", + "logs": "লগ" + }, + "launcherLogin": { + "title": "সাইন ইন", + "description": "চালিয়ে যেতে ড্যাশবোর্ড পাসওয়ার্ড দিন।", + "passwordLabel": "পাসওয়ার্ড", + "passwordPlaceholder": "পাসওয়ার্ড লিখুন", + "submit": "সাইন ইন", + "errorInvalid": "ভুল পাসওয়ার্ড। আবার চেষ্টা করুন।", + "errorNetwork": "নেটওয়ার্ক ত্রুটি। আবার চেষ্টা করুন।" + }, + "launcherSetup": { + "title": "ড্যাশবোর্ড পাসওয়ার্ড সেট করুন", + "description": "এই ড্যাশবোর্ডে অ্যাক্সেস সুরক্ষিত রাখতে একটি পাসওয়ার্ড বেছে নিন। প্রতিবার সাইন ইনের সময় এটি ব্যবহার করতে হবে।", + "passwordLabel": "পাসওয়ার্ড", + "passwordPlaceholder": "কমপক্ষে ৮ অক্ষর", + "confirmLabel": "পাসওয়ার্ড নিশ্চিত করুন", + "confirmPlaceholder": "পাসওয়ার্ড আবার লিখুন", + "submit": "পাসওয়ার্ড সেট করুন", + "errorMismatch": "পাসওয়ার্ড মিলছে না।", + "errorNetwork": "নেটওয়ার্ক ত্রুটি। আবার চেষ্টা করুন।" + }, + "chat": { + "welcome": "আজ আমি আপনাকে কীভাবে সাহায্য করতে পারি?", + "welcomeDesc": "আবহাওয়া, সেটিংস বা অন্য যেকোনো কাজ সম্পর্কে আমাকে জিজ্ঞাসা করুন। আমি সাহায্যের জন্য এখানে আছি।", + "placeholder": "একটি নতুন বার্তা শুরু করুন...", + "disabledPlaceholder": { + "gatewayUnknown": "চ্যাট করা যাচ্ছে না: গেটওয়ের স্ট্যাটাস এখনও পরীক্ষা করা হচ্ছে। অনুগ্রহ করে অপেক্ষা করুন, তারপর পৃষ্ঠাটি রিফ্রেশ করুন অথবা প্রয়োজনে লঞ্চার পুনরায় চালু করুন।", + "gatewayStarting": "চ্যাট করা যাচ্ছে না: গেটওয়ে চালু হচ্ছে। চালু হওয়া শেষ হওয়া পর্যন্ত অপেক্ষা করুন, তারপর আবার চেষ্টা করুন।", + "gatewayRestarting": "চ্যাট করা যাচ্ছে না: গেটওয়ে পুনরায় চালু হচ্ছে। অনুগ্রহ করে পুনরায় চালু হওয়া শেষ হওয়া পর্যন্ত অপেক্ষা করুন।", + "gatewayStopping": "চ্যাট করা যাচ্ছে না: গেটওয়ে বন্ধ হচ্ছে। বন্ধ হওয়া পর্যন্ত অপেক্ষা করুন, তারপর গেটওয়ে আবার চালু করুন।", + "gatewayStopped": "চ্যাট করা যাচ্ছে না: গেটওয়ে শুরু করা হয়নি। উপরের বারে স্টার্ট গেটওয়েতে ক্লিক করুন, তারপর পুনরায় চেষ্টা করুন।", + "gatewayError": "চ্যাট করা যাচ্ছে না: গেটওয়ে ত্রুটি অবস্থায় রয়েছে। লগ পরীক্ষা করুন, তারপর গেটওয়ে বা লঞ্চার পুনরায় চালু করুন।", + "websocketConnecting": "চ্যাট সার্ভিসের সাথে সংযোগ স্থাপন হচ্ছে... অনুগ্রহ করে অপেক্ষা করুন।", + "websocketDisconnected": "চ্যাট করা যাচ্ছে না: WebSocket সংযোগ বিচ্ছিন্ন। নেটওয়ার্ক এবং গেটওয়ের অবস্থা পরীক্ষা করুন, তারপর পৃষ্ঠাটি রিফ্রেশ করুন বা লঞ্চার পুনরায় চালু করুন।", + "websocketError": "চ্যাট করা যাচ্ছে না: WebSocket সংযোগ ব্যর্থ হয়েছে। নেটওয়ার্ক এবং গেটওয়ের অবস্থা পরীক্ষা করুন, তারপর পুনরায় চেষ্টা করুন।", + "noDefaultModel": "চ্যাট করা যাচ্ছে না: কোনো ডিফল্ট মডেল নির্বাচিত নেই। মডেল পৃষ্ঠায় একটি ডিফল্ট মডেল সেট করুন।" + }, + "newChat": "নতুন চ্যাট", + "notConnected": "গেটওয়ে চলছে না। চ্যাট করতে এটি চালু করুন।", + "thinking": { + "step1": "চিন্তা করা হচ্ছে...", + "step2": "আপনার অনুরোধ বিশ্লেষণ করা হচ্ছে...", + "step3": "উত্তর প্রস্তুত করা হচ্ছে...", + "step4": "প্রায় শেষ..." + }, + "reasoningLabel": "যুক্তি", + "toolCallsLabel": "টুল কল", + "toolCallExplanationLabel": "কল নোট", + "toolCallFunctionLabel": "কল সারাংশ", + "toolCallArgumentsLabel": "আর্গুমেন্ট", + "showAssistantDetails": "যুক্তি এবং টুল কল", + "assistantDetailVisibility": { + "none": "কোনোটিই দেখাবেন না", + "thought": "শুধু যুক্তি দেখান", + "toolCalls": "শুধু টুল কল দেখান", + "all": "উভয়ই দেখান" + }, + "toolLabel": "টুল", + "codeLabel": "কোড", + "copyMessage": "বার্তা কপি করুন", + "copyCode": "কোড কপি করুন", + "copiedLabel": "কপি করা হয়েছে", + "enableCodeWrap": "লাইন র‍্যাপ করুন", + "disableCodeWrap": "র‍্যাপ নিষ্ক্রিয় করুন", + "expandCode": "কোড প্রসারিত করুন", + "collapseCode": "কোড সংকুচিত করুন", + "history": "ইতিহাস", + "noHistory": "এখনও কোনো চ্যাট ইতিহাস নেই", + "historyLoadFailed": "চ্যাট ইতিহাস লোড করতে ব্যর্থ", + "historyOpenFailed": "এই চ্যাট ইতিহাস খুলতে ব্যর্থ", + "loadingMore": "আরও লোড হচ্ছে...", + "deleteSession": "সেশন মুছুন", + "messagesCount": "{{count}}টি বার্তা", + "noModel": "মডেল নির্বাচন করুন", + "inputDisabled": { + "notConnected": "গেটওয়ে চলছে না। চ্যাট করতে এটি চালু করুন।", + "noModel": "কোনো ডিফল্ট মডেল কনফিগার করা নেই। একটি সেট করতে মডেল পৃষ্ঠায় যান।" + }, + "sendMessage": "বার্তা পাঠান", + "sendHint": "পাঠাতে Enter চাপুন\nনতুন লাইনের জন্য Shift + Enter", + "contextTitle": "প্রসঙ্গ", + "contextDetail": "বিস্তারিত দেখুন", + "attachImage": "ছবি যোগ করুন", + "removeImage": "ছবি সরান", + "uploadedImage": "আপলোড করা ছবি", + "invalidImage": "\"{{name}}\" একটি সমর্থিত ছবি ফাইল নয়।", + "imageTooLarge": "\"{{name}}\" {{size}} সীমা অতিক্রম করেছে।", + "imageReadFailed": "\"{{name}}\" পড়তে ব্যর্থ হয়েছে।", + "empty": { + "noConfiguredModel": "কোনো মডেল কনফিগার করা নেই", + "noConfiguredModelDescription": "চ্যাট শুরু করার আগে আপনাকে কমপক্ষে একটি AI মডেল API কী দিয়ে কনফিগার করতে হবে।", + "goToModels": "মডেলে যান", + "noSelectedModel": "কোনো মডেল নির্বাচিত নেই", + "noSelectedModelDescription": "আপনি মডেল কনফিগার করেছেন, কিন্তু কোনোটি ডিফল্ট হিসেবে সেট করা নেই। চ্যাট শুরু করার আগে একটি মডেল নির্বাচন করুন।", + "notRunning": "গেটওয়ে চলছে না", + "notRunningDescription": "চ্যাট শুরু করতে গেটওয়ে সার্ভিস চালু করুন। উপরের বারে স্টার্ট গেটওয়ে বোতাম ব্যবহার করুন।" + }, + "modelGroup": { + "apikey": "API Key", + "oauth": "OAuth", + "local": "লোকাল" + } + }, + "header": { + "logout": { + "tooltip": "সাইন আউট", + "confirm": "সাইন আউট", + "description": "আপনি কি ড্যাশবোর্ড থেকে সাইন আউট করতে চান?" + }, + "gateway": { + "stopDialog": { + "title": "গেটওয়ে সার্ভিস বন্ধ করবেন?", + "description": "আপনি কি গেটওয়ে বন্ধ করতে চান? এটি আপনার সক্রিয় চ্যাট সেশন বিচ্ছিন্ন করবে এবং ইনফারেন্স থামাবে।", + "confirm": "গেটওয়ে বন্ধ করুন" + }, + "action": { + "start": "গেটওয়ে চালু করুন", + "stop": "গেটওয়ে বন্ধ করুন", + "restart": "গেটওয়ে পুনরায় চালু করুন" + }, + "status": { + "starting": "গেটওয়ে চালু হচ্ছে...", + "restarting": "গেটওয়ে পুনরায় চালু হচ্ছে...", + "stopping": "গেটওয়ে বন্ধ হচ্ছে..." + }, + "restartRequired": "কনফিগারেশনের পরিবর্তন কার্যকর হতে গেটওয়ে পুনরায় চালু করা প্রয়োজন।" + } + }, + "common": { + "cancel": "বাতিল", + "close": "বন্ধ করুন", + "save": "সংরক্ষণ করুন", + "saving": "সংরক্ষণ করা হচ্ছে...", + "reset": "রিসেট", + "confirm": "নিশ্চিত করুন", + "fix": "ঠিক করুন", + "saveChangesTitle": "আপনার অসংরক্ষিত কনফিগারেশন পরিবর্তন রয়েছে", + "restartRequiredTitle": "গেটওয়ে পুনরায় চালু করা প্রয়োজন", + "restartRequiredDesc": "সর্বশেষ {{name}} কনফিগারেশন সংরক্ষিত হয়েছে। কার্যকর হতে গেটওয়ে পুনরায় চালু করুন।" + }, + "labels": { + "loading": "লোড হচ্ছে..." + }, + "footer": { + "version": "সংস্করণ", + "commit": "কমিট", + "build": "বিল্ড", + "version_unknown": "অজানা" + }, + "credentials": { + "description": "সমর্থিত প্রোভাইডারদের জন্য OAuth এবং টোকেন-ভিত্তিক ক্রেডেনশিয়াল পরিচালনা করুন।", + "loading": "ক্রেডেনশিয়াল লোড হচ্ছে...", + "providers": { + "openai": { + "description": "ব্রাউজার OAuth, ডিভাইস কোড এবং টোকেন লগইন সমর্থন করে।" + }, + "anthropic": { + "description": "Claude অ্যাক্সেসের জন্য টোকেন লগইন ব্যবহার করে।" + }, + "antigravity": { + "description": "Google Cloud Code Assist-এর জন্য ব্রাউজার OAuth ব্যবহার করে।" + } + }, + "status": { + "connected": "সংযুক্ত", + "needsRefresh": "রিফ্রেশ প্রয়োজন", + "expired": "মেয়াদ শেষ", + "notLoggedIn": "লগ ইন করা নেই" + }, + "actions": { + "browser": "ব্রাউজার OAuth", + "deviceCode": "ডিভাইস কোড", + "stopLoading": "লোডিং বন্ধ করুন", + "saveToken": "সংরক্ষণ করুন", + "logout": "লগআউট" + }, + "logoutDialog": { + "title": "প্রোভাইডার থেকে লগআউট করবেন?", + "description": "এটি {{provider}}-এর জন্য সংরক্ষিত আপনার ক্রেডেনশিয়াল মুছে ফেলবে।" + }, + "fields": { + "openaiToken": "OpenAI টোকেন", + "anthropicToken": "Anthropic টোকেন" + }, + "labels": { + "account": "অ্যাকাউন্ট", + "email": "ইমেল", + "project": "প্রকল্প" + }, + "errors": { + "loadFailed": "ক্রেডেনশিয়াল লোড করতে ব্যর্থ", + "flowFailed": "প্রমাণীকরণ প্রবাহ পরীক্ষা করতে ব্যর্থ", + "loginFailed": "লগইন ব্যর্থ", + "logoutFailed": "লগআউট ব্যর্থ", + "invalidBrowserResponse": "ব্রাউজার লগইন প্রতিক্রিয়া অবৈধ", + "invalidDeviceResponse": "ডিভাইস কোড প্রতিক্রিয়া অবৈধ", + "popupBlocked": "নতুন ট্যাব খোলা যাচ্ছে না। অনুগ্রহ করে পপআপ অনুমতি দিন এবং আবার চেষ্টা করুন।" + }, + "flow": { + "current": "বর্তমান প্রমাণীকরণ অবস্থা", + "pending": "অনুমোদনের জন্য অপেক্ষা করা হচ্ছে...", + "success": "প্রমাণীকরণ সফল", + "error": "প্রমাণীকরণ ব্যর্থ", + "expired": "প্রমাণীকরণ সেশনের মেয়াদ শেষ" + }, + "device": { + "title": "OpenAI ডিভাইস লগইন", + "description": "যাচাইকরণ পৃষ্ঠা খুলুন এবং নিচের কোডটি লিখুন। এই পৃষ্ঠাটি স্বয়ংক্রিয়ভাবে রিফ্রেশ হবে।", + "code": "ইউজার কোড", + "url": "যাচাইকরণ URL", + "polling": "লগইন অবস্থা পোলিং করা হচ্ছে...", + "open": "যাচাইকরণ পৃষ্ঠা খুলুন" + } + }, + "models": { + "description": "AI প্রোভাইডারদের জন্য API কী কনফিগার করুন। শুধুমাত্র কনফিগার করা মডেলগুলি চ্যাটের জন্য উপলব্ধ।", + "defaultChangeSuccess": "ডিফল্ট মডেল আপডেট করা হয়েছে।", + "unsavedPrompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। মডেল কনফিগারেশনে লিখতে সংরক্ষণ করুন।", + "restartHint": "মডেল কনফিগারেশনের পরিবর্তন গেটওয়ে পুনরায় চালু হওয়ার পরে কার্যকর হয়।", + "loadError": "মডেল লোড করতে ব্যর্থ", + "retry": "পুনরায় চেষ্টা করুন", + "providerCatalogUnavailable": "ব্যাকএন্ড প্রোভাইডার ক্যাটালগ অনুপলব্ধ। মডেল API সফলভাবে লোড না হওয়া পর্যন্ত নতুন প্রোভাইডার নির্বাচন নিষ্ক্রিয়।", + "noDefaultHintPrefix": "এখনও কোনো ডিফল্ট মডেল সেট করা নেই। সেট করতে ক্লিক করুন", + "noDefaultHintSuffix": "এটি সেট করতে।", + "status": { + "available": "উপলব্ধ", + "unconfigured": "কনফিগার করা নেই", + "unreachable": "সার্ভিস অপ্রাপ্য" + }, + "badge": { + "default": "ডিফল্ট", + "virtual": "ভার্চুয়াল" + }, + "action": { + "edit": "API কী সম্পাদনা করুন", + "setDefault": "ডিফল্ট হিসেবে সেট করুন", + "delete": "মডেল মুছুন", + "setDefaultDisabled": { + "setting": "ডিফল্ট হিসেবে সেট করা হচ্ছে...", + "unavailable": "অনুপলব্ধ মডেলকে ডিফল্ট হিসেবে সেট করা যাবে না", + "isDefault": "ইতিমধ্যে ডিফল্ট মডেল", + "isVirtual": "ভার্চুয়াল মডেলকে ডিফল্ট হিসেবে সেট করা যাবে না", + "unsupportedProvider": "এই প্রোভাইডারকে ডিফল্ট চ্যাট মডেল হিসেবে ব্যবহার করা যাবে না।" + }, + "deleteDisabled": { + "isDefault": "ডিফল্ট মডেল মুছে ফেলা যাবে না" + } + }, + "defaultOnSave": { + "label": "ডিফল্ট মডেল", + "description": "সংরক্ষণের পরে স্বয়ংক্রিয়ভাবে এই মডেলটিকে ডিফল্ট হিসেবে সেট করুন।", + "unsupportedProvider": "এই প্রোভাইডারকে মডেল তালিকায় সংরক্ষণ করা যাবে কিন্তু ডিফল্ট চ্যাট মডেল হিসেবে ব্যবহার করা যাবে না।" + }, + "add": { + "button": "মডেল যোগ করুন", + "title": "কাস্টম মডেল যোগ করুন", + "description": "একটি OpenAI-সামঞ্জস্যপূর্ণ বা নেটিভ মডেল এন্ডপয়েন্ট যোগ করুন।", + "modelName": "মডেল এলিয়াস", + "modelNamePlaceholder": "যেমন my-gpt4", + "modelNameHint": "কথোপকথনে এই মডেলটি চিহ্নিত করতে ব্যবহৃত একটি ছোট নাম।", + "modelId": "মডেল শনাক্তকারী", + "modelIdPlaceholder": "যেমন gpt-4o বা openai/gpt-4o", + "modelIdHint": "এই ক্ষেত্রটি নির্বাচিত প্রোভাইডারের জন্য canonical মডেল ID হিসেবে বিবেচিত হয়। যদি শনাক্তকারীতে নিজেই একটি স্ল্যাশ থাকে (উদাহরণস্বরূপ openai/gpt-5.4), তবে এটি অপরিবর্তিত রাখা হয় এবং আবার বিভক্ত করা হয় না।", + "errorRequired": "এই ক্ষেত্রটি প্রয়োজনীয়।", + "errorDuplicateModelName": "মডেল এলিয়াস ইতিমধ্যে বিদ্যমান। অনুগ্রহ করে একটি ভিন্ন নাম ব্যবহার করুন।", + "saveError": "মডেল যোগ করতে ব্যর্থ", + "saveSuccess": "মডেল যোগ করা হয়েছে।", + "confirm": "মডেল যোগ করুন" + }, + "delete": { + "title": "মডেল মুছবেন?", + "description": "\"{{name}}\" আপনার মডেল তালিকা থেকে স্থায়ীভাবে সরানো হবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।", + "confirm": "মুছুন" + }, + "advanced": { + "toggle": "উন্নত বিকল্প" + }, + "field": { + "provider": "Provider", + "providerPlaceholder": "একটি প্রোভাইডার নির্বাচন করুন", + "providerHint": "ব্যাকএন্ড ক্যাটালগ থেকে একটি প্রোভাইডার নির্বাচন করুন; মডেল শনাক্তকারীকে সেই প্রোভাইডারের canonical মডেল ID হিসেবে ব্যাখ্যা করা হবে।", + "providerInvalid": "বর্তমান প্রোভাইডার অবৈধ। অনুগ্রহ করে একটি সমর্থিত প্রোভাইডার নির্বাচন করুন।", + "selectProviderFirst": "প্রথমে একটি প্রোভাইডার নির্বাচন করুন", + "apiBase": "API Base URL", + "apiKey": "API Key", + "apiKeyPlaceholder": "আপনার API কী লিখুন", + "apiKeyPlaceholderSet": "বিদ্যমান কী রাখতে খালি রাখুন", + "proxy": "HTTP প্রক্সি", + "proxyHint": "ঐচ্ছিক। যেমন http://127.0.0.1:7890", + "authMethod": "প্রমাণীকরণ পদ্ধতি", + "authMethodHint": "প্রমাণীকরণ পদ্ধতি: oauth, token। API কী প্রমাণীকরণের জন্য খালি রাখুন।", + "authMethodManagedHint": "এই প্রোভাইডারের প্রমাণীকরণ পদ্ধতি সিস্টেম দ্বারা পরিচালিত।", + "connectMode": "সংযোগ মোড", + "connectModeHint": "CLI-ভিত্তিক প্রোভাইডারদের জন্য সংযোগ মোড: stdio বা grpc।", + "workspace": "ওয়ার্কস্পেস পাথ", + "workspaceHint": "CLI-ভিত্তিক প্রোভাইডারদের জন্য কাজের ডিরেক্টরি (যেমন GitHub Copilot)।", + "requestTimeout": "অনুরোধ টাইমআউট (সেকেন্ড)", + "requestTimeoutHint": "উত্তরের জন্য অপেক্ষা করার সর্বাধিক সেকেন্ড। 0 = ডিফল্ট ব্যবহার করুন।", + "rpm": "রেট সীমা (RPM)", + "rpmHint": "প্রতি মিনিটে সর্বাধিক অনুরোধ। 0 = কোনো সীমা নেই।", + "thinkingLevel": "চিন্তার স্তর", + "thinkingLevelHint": "thinking_level বাদ দিতে এবং প্রোভাইডার ডিফল্ট ব্যবহার করতে খালি রাখুন। মান: off, low, medium, high, xhigh, adaptive।", + "providerDefault": "প্রোভাইডার ডিফল্ট", + "maxTokensField": "ম্যাক্স টোকেন ফিল্ড", + "maxTokensFieldHint": "ম্যাক্স টোকেনের জন্য অনুরোধ ক্ষেত্রের নাম ওভাররাইড করুন, যেমন max_completion_tokens।", + "toolSchemaTransform": "টুল স্কিমা ট্রান্সফর্ম", + "toolSchemaTransformHint": "টুল JSON স্কিমার জন্য ঐচ্ছিক সামঞ্জস্যতা ট্রান্সফর্ম। নেটিভ আচরণের জন্য খালি রাখুন। সমর্থিত মান: simple।", + "streamingEnabled": "স্ট্রিমিং আউটপুট", + "streamingEnabledHint": "এই মডেল এন্ট্রিকে প্রোভাইডার স্ট্রিমিং অনুরোধ চেষ্টা করার অনুমতি দিন। বর্তমান চ্যানেল স্ট্রিমিং সুইচও সক্ষম থাকতে হবে।", + "extraBody": "Extra Body", + "extraBodyHint": "অনুরোধ বডিতে ইনজেক্ট করার অতিরিক্ত JSON ক্ষেত্র, যেমন {\"reasoning_split\": true}।", + "customHeaders": "Custom Headers", + "customHeadersHint": "প্রতিটি অনুরোধে ইনজেক্ট করার অতিরিক্ত HTTP হেডার, যেমন {\"X-Source\": \"coding-plan\"}।", + "invalidJson": "অবৈধ JSON ফর্ম্যাট" + }, + "edit": { + "title": "{{name}} কনফিগার করুন", + "apiKeyHint": "একটি কী ইতিমধ্যে সেট করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।", + "oauthNote": "এই প্রোভাইডার OAuth ব্যবহার করে — কোনো API কী প্রয়োজন নেই।", + "saveError": "সংরক্ষণ করতে ব্যর্থ", + "saveSuccess": "মডেল কনফিগারেশন সংরক্ষিত হয়েছে।" + }, + "fetch": { + "title": "উপলব্ধ মডেল আনুন", + "description": "আপস্ট্রিম প্রোভাইডার থেকে মডেল তালিকা আনুন।", + "providerLabel": "প্রোভাইডার:", + "needApiKey": "মডেল আনতে অনুগ্রহ করে প্রথমে একটি API কী লিখুন।", + "fetching": "মডেল আনা হচ্ছে...", + "retry": "পুনরায় চেষ্টা করুন", + "filterPlaceholder": "মডেল ফিল্টার করুন...", + "found": "{{count}}টি মডেল পাওয়া গেছে", + "found_plural": "{{count}}টি মডেল পাওয়া গেছে", + "shown": "({{count}}টি দেখানো হয়েছে)", + "selectAll": "সব নির্বাচন করুন", + "deselectAll": "সব নির্বাচন বাতিল করুন", + "fill": "{{count}}টি নির্বাচিত মডেল পূরণ করুন", + "fill_plural": "{{count}}টি নির্বাচিত মডেল পূরণ করুন", + "failed": "মডেল আনতে ব্যর্থ" + }, + "catalog": { + "button": "সংরক্ষিত ক্যাটালগ", + "title": "সংরক্ষিত মডেল ক্যাটালগ", + "description": "পূর্বে আনা মডেল তালিকা, প্রতি API কী অনুযায়ী সংরক্ষিত। আপনার কনফিগারেশনে যোগ করতে মডেল নির্বাচন করুন।", + "loading": "ক্যাটালগ লোড হচ্ছে...", + "empty": "এখনও কোনো সংরক্ষিত ক্যাটালগ নেই। একটি ক্যাটালগ সংরক্ষণ করতে প্রোভাইডার থেকে মডেল আনুন।", + "filterPlaceholder": "মডেল ফিল্টার করুন...", + "models": "মডেল", + "fetchedAt": "আনা হয়েছে", + "delete": "ক্যাটালগ মুছুন", + "refresh": "আপস্ট্রিম থেকে রিফ্রেশ করুন", + "found": "{{count}}টি মডেল পাওয়া গেছে", + "found_plural": "{{count}}টি মডেল পাওয়া গেছে", + "selectAll": "সব নির্বাচন করুন", + "deselectAll": "সব নির্বাচন বাতিল করুন", + "addSelected": "{{count}}টি নির্বাচিত যোগ করুন", + "addSuccess": "কনফিগারেশনে {{count}}টি মডেল যোগ করা হয়েছে।", + "needApiKey": "এই মডেলগুলির একটি API কী প্রয়োজন। আমদানির পরে আপনাকে ক্রেডেনশিয়াল কনফিগার করতে হবে।" + }, + "test": { + "title": "মডেল সংযোগ পরীক্ষা করুন", + "description": "যাচাই করুন যে মডেল এন্ডপয়েন্টটি অ্যাক্সেসযোগ্য এবং সঠিকভাবে কনফিগার করা হয়েছে।", + "modelLabel": "মডেল:", + "identifierLabel": "শনাক্তকারী:", + "endpointLabel": "এন্ডপয়েন্ট:", + "testConnection": "সংযোগ পরীক্ষা করুন", + "testing": "সংযোগ পরীক্ষা করা হচ্ছে...", + "success": "সংযোগ সফল", + "responseTime": "প্রতিক্রিয়া সময়: {{ms}}ms", + "failed": "সংযোগ ব্যর্থ", + "status": "স্ট্যাটাস: {{status}}", + "testFailed": "পরীক্ষা ব্যর্থ", + "testAgain": "আবার পরীক্ষা করুন" + }, + "validation": { + "whitespace": "মডেল শনাক্তকারীতে স্পেস থাকতে পারবে না", + "leadingSlash": "/ দিয়ে শুরু হওয়া উচিত নয়", + "consecutiveSlash": "পরপর / থাকা উচিত নয়", + "useProvider": "প্রোভাইডার হিসেবে \"{{provider}}\" ব্যবহার করা হবে", + "defaultToOpenAI": "কোনো প্রোভাইডার নির্দিষ্ট করা নেই, ডিফল্ট OpenAI", + "emptyModel": "মডেলের নাম খালি হতে পারবে না", + "shouldUse": "\"{{provider}}\" এর \"{{alias}}\" ব্যবহার করা উচিত", + "didYouMean": "আপনি কি \"{{closest}}\" বোঝাতে চেয়েছেন?", + "unknownProvider": "অজানা প্রোভাইডার \"{{provider}}\"", + "parsed": "প্রোভাইডার={{provider}}, মডেল={{model}}" + }, + "combobox": { + "selectProvider": "প্রোভাইডার নির্বাচন করুন...", + "searchProvider": "প্রোভাইডার অনুসন্ধান করুন...", + "noProvider": "কোনো প্রোভাইডার পাওয়া যায়নি।", + "noCatalog": "প্রোভাইডার ক্যাটালগ অনুপলব্ধ।", + "local": "লোকাল" + } + }, + "channels": { + "loadError": "চ্যানেল লোড করতে ব্যর্থ", + "name": { + "telegram": "Telegram", + "discord": "Discord", + "slack": "Slack", + "feishu": "ফেইশু", + "dingtalk": "ডিংটক", + "line": "LINE", + "qq": "QQ", + "onebot": "OneBot", + "wecom": "উইকম", + "whatsapp": "WhatsApp", + "whatsapp_native": "WhatsApp Native", + "pico": "Web", + "maixcam": "MaixCam", + "matrix": "Matrix", + "irc": "IRC", + "weixin": "উইচ্যাট", + "mqtt": "MQTT" + }, + "weixin": { + "bindTitle": "WeChat অ্যাকাউন্ট বাইন্ডিং", + "bindDesc": "আপনার ব্যক্তিগত অ্যাকাউন্ট বাইন্ড করতে WeChat দিয়ে QR কোড স্ক্যান করুন।", + "bind": "WeChat বাইন্ড করুন", + "rebind": "পুনরায় বাইন্ড করুন", + "bound": "WeChat বাইন্ড করা হয়েছে", + "notBound": "WeChat অ্যাকাউন্ট এখনও বাইন্ড করা হয়নি।", + "generating": "QR কোড তৈরি করা হচ্ছে...", + "scanHint": "WeChat খুলুন এবং QR কোড স্ক্যান করুন", + "scanned": "স্ক্যান করা হয়েছে — অনুগ্রহ করে WeChat-এ নিশ্চিত করুন", + "expired": "QR কোডের মেয়াদ শেষ", + "retry": "আবার চেষ্টা করুন", + "refresh": "QR রিফ্রেশ করুন", + "errorGeneric": "একটি ত্রুটি ঘটেছে। অনুগ্রহ করে আবার চেষ্টা করুন।" + }, + "wecom": { + "bindTitle": "WeCom বাইন্ডিং", + "bindDesc": "আপনার AI বট বাইন্ড করতে WeCom দিয়ে QR কোড স্ক্যান করুন।", + "bind": "WeCom বাইন্ড করুন", + "rebind": "পুনরায় বাইন্ড করুন", + "bound": "WeCom বাইন্ড করা হয়েছে", + "notBound": "WeCom AI বট এখনও বাইন্ড করা হয়নি।", + "generating": "QR কোড তৈরি করা হচ্ছে...", + "scanHint": "WeCom খুলুন এবং QR কোড স্ক্যান করুন", + "scanned": "স্ক্যান করা হয়েছে, অনুগ্রহ করে WeCom-এ নিশ্চিত করুন", + "expired": "QR কোডের মেয়াদ শেষ", + "retry": "আবার চেষ্টা করুন", + "refresh": "QR রিফ্রেশ করুন", + "errorGeneric": "একটি ত্রুটি ঘটেছে। অনুগ্রহ করে আবার চেষ্টা করুন।" + }, + "field": { + "token": "Bot Token", + "tokenPlaceholder": "বট টোকেন লিখুন", + "botToken": "Bot Token", + "appToken": "App Token", + "appId": "App ID", + "appSecret": "App Secret", + "verificationToken": "Verification Token", + "encryptKey": "Encrypt Key", + "baseUrl": "API Base URL", + "proxy": "HTTP প্রক্সি", + "mentionOnly": "শুধুমাত্র উল্লেখ করলে", + "typingEnabled": "টাইপিং সূচক", + "placeholderEnabled": "প্লেসহোল্ডার বার্তা", + "placeholderText": "প্লেসহোল্ডার টেক্সট", + "streamingEnabled": "স্ট্রিমিং আউটপুট", + "streamingThrottleSeconds": "আপডেট ব্যবধান (সেকেন্ড)", + "streamingMinGrowthChars": "ন্যূনতম বৃদ্ধির অক্ষর", + "groupTriggerMentionOnly": "গ্রুপে শুধুমাত্র উল্লেখ", + "groupTriggerPrefixes": "গ্রুপ ট্রিগার প্রিফিক্স", + "groupTriggerPrefixesPlaceholder": "যেমন /, !, ?", + "randomReactionEmoji": "র‍্যান্ডম প্রতিক্রিয়া ইমোজি", + "randomReactionEmojiPlaceholder": "যেমন THUMBSUP, HEART, SMILE", + "isLark": "Lark (আন্তর্জাতিক)", + "allowFrom": "যাদের থেকে অনুমতি", + "allowFromPlaceholder": "যেমন 123456, 789012", + "allowOrigins": "অনুমোদিত অরিজিন", + "allowOriginsPlaceholder": "যেমন https://example.com, http://localhost:5173", + "removeListItem": "{{value}} সরান", + "secretPlaceholder": "সিক্রেট লিখুন", + "secretHintSet": "একটি মান ইতিমধ্যে সেট করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।" + }, + "page": { + "notFound": "চ্যানেল \"{{name}}\" সমর্থিত নয়।", + "saveSuccess": "চ্যানেল কনফিগারেশন সংরক্ষিত হয়েছে।", + "saveError": "চ্যানেল কনফিগারেশন সংরক্ষণ করতে ব্যর্থ", + "savePrompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। চ্যানেল কনফিগারেশনে লিখতে সংরক্ষণ করুন।", + "docLink": "ডকুমেন্টেশন", + "enableLabel": "চ্যানেল সক্ষম করুন", + "restartRequiredTitle": "গেটওয়ে পুনরায় চালু করা প্রয়োজন", + "restartRequiredDesc": "সর্বশেষ {{name}} কনফিগারেশন সংরক্ষিত হয়েছে। কার্যকর হতে গেটওয়ে পুনরায় চালু করুন।" + }, + "form": { + "desc": { + "token": "প্ল্যাটফর্ম API-এর সাথে সংযোগের জন্য ব্যবহৃত বট অ্যাক্সেস টোকেন।", + "botToken": "বার্তা পাঠাতে এবং গ্রহণ করতে ব্যবহৃত বট টোকেন।", + "appToken": "Socket Mode সংযোগের জন্য ব্যবহৃত অ্যাপ টোকেন।", + "appId": "প্রমাণীকরণের জন্য ব্যবহৃত অনন্য অ্যাপ্লিকেশন ID।", + "appSecret": "স্বাক্ষর এবং প্রমাণীকরণের জন্য ব্যবহৃত অ্যাপ্লিকেশন সিক্রেট।", + "verificationToken": "ইভেন্ট কলব্যাকের জন্য যাচাইকরণ টোকেন।", + "encryptKey": "কলব্যাক পেলোড ডিক্রিপ্ট করতে ব্যবহৃত এনক্রিপশন কী।", + "baseUrl": "প্ল্যাটফর্ম API বেস URL। ডিফল্টরূপে অফিসিয়াল এন্ডপয়েন্ট ব্যবহৃত হয়।", + "proxy": "বহির্গামী নেটওয়ার্ক অ্যাক্সেসের জন্য HTTP প্রক্সি ঠিকানা।", + "mentionOnly": "গ্রুপ চ্যাটে শুধুমাত্র বটকে স্পষ্টভাবে উল্লেখ করলে প্রতিক্রিয়া জানান।", + "typingEnabled": "সহকারী প্রতিক্রিয়া তৈরি করার সময় টাইপিং স্ট্যাটাস প্রদর্শন করুন।", + "placeholderEnabled": "চূড়ান্ত উত্তর পাঠানোর আগে অস্থায়ী প্লেসহোল্ডার বার্তা সক্ষম করুন।", + "streamingEnabled": "এই চ্যানেলকে প্রোভাইডার স্ট্রিমিং আউটপুট প্রদর্শনের অনুমতি দিন। বর্তমান মডেল এন্ট্রি স্ট্রিমিং সুইচও সক্ষম থাকতে হবে।", + "streamingThrottleSeconds": "মধ্যবর্তী স্ট্রিমিং আপডেটগুলির মধ্যে ন্যূনতম ব্যবধান। 0 মানে ডিফল্ট ব্যবহার করুন। চূড়ান্ত উত্তরে থ্রটল করা হয় না।", + "streamingMinGrowthChars": "অন্য একটি মধ্যবর্তী স্ট্রিমিং আপডেট পাঠানোর আগে ন্যূনতম টেক্সট বৃদ্ধি। 0 মানে ডিফল্ট ব্যবহার করুন। চূড়ান্ত উত্তরে থ্রটল করা হয় না।", + "groupTriggerMentionOnly": "গ্রুপ চ্যাটে, শুধুমাত্র বটকে উল্লেখ করলেই প্রতিক্রিয়া জানান।", + "groupTriggerPrefixes": "কাস্টম গ্রুপ-চ্যাট ট্রিগার প্রিফিক্স। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।", + "randomReactionEmoji": "PicoClaw প্রাপ্তি নিশ্চিত করতে ব্যবহারকারীর বার্তায় ইমোজি প্রতিক্রিয়া যোগ করে। উদাহরণ: \"THUMBSUP\", \"HEART\", \"SMILE\"। ডিফল্ট \"Pin\" ইমোজি ব্যবহার করতে খালি রাখুন।", + "isLark": "Feishu ডোমেইন (open.feishu.cn) এর পরিবর্তে Lark আন্তর্জাতিক ডোমেইন (open.larksuite.com) ব্যবহার করুন।", + "allowFrom": "অনুমোদিত ব্যবহারকারী বা গ্রুপ ID। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।", + "allowOrigins": "অনুমোদিত অরিজিন ডোমেইন। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।", + "wsUrl": "WebSocket সার্ভিস URL।", + "reconnectInterval": "বিচ্ছিন্ন হওয়ার পরে পুনঃসংযোগের ব্যবধান (সেকেন্ড)।", + "bridgeUrl": "ব্রিজ সার্ভিস URL।", + "sessionStorePath": "সেশন স্টোরেজের জন্য লোকাল পাথ।", + "useNative": "নেটিভ ক্লায়েন্ট মোড ব্যবহার করবেন কিনা।", + "host": "সার্ভিস হোস্ট ঠিকানা।", + "port": "সার্ভিস পোর্ট।", + "homeserver": "Matrix হোমসার্ভার URL।", + "userId": "অ্যাকাউন্ট ইউজার ID।", + "deviceId": "ডিভাইস ID।", + "joinOnInvite": "আমন্ত্রিত হলে স্বয়ংক্রিয়ভাবে রুমে যোগ দিন।", + "clientId": "প্ল্যাটফর্ম প্রমাণীকরণের জন্য ব্যবহৃত ক্লায়েন্ট ID।", + "corpId": "এন্টারপ্রাইজ Corp ID।", + "agentId": "এন্টারপ্রাইজ অ্যাপ্লিকেশন এজেন্ট ID।", + "webhookUrl": "সম্পূর্ণ webhook URL।", + "webhookHost": "Webhook শোনার হোস্ট।", + "webhookPort": "Webhook শোনার পোর্ট।", + "webhookPath": "Webhook রুট পাথ।", + "replyTimeout": "সেকেন্ডে উত্তরের টাইমআউট।", + "maxSteps": "প্রক্রিয়াকরণ ধাপের সর্বাধিক সংখ্যা।", + "welcomeMessage": "নতুন সেশনের জন্য স্বাগত বার্তার বিষয়বস্তু।", + "allowTokenQuery": "URL কোয়েরি প্যারামিটারে টোকেন অনুমতি দিন।", + "pingInterval": "সেকেন্ডে সংযোগ হার্টবিট ব্যবধান।", + "readTimeout": "সেকেন্ডে রিড টাইমআউট।", + "writeTimeout": "সেকেন্ডে রাইট টাইমআউট।", + "maxConnections": "একযোগে সংযোগের সর্বাধিক সংখ্যা।", + "server": "IRC সার্ভার ঠিকানা।", + "tls": "TLS সক্ষম করবেন কিনা।", + "nick": "বট ডাকনাম।", + "user": "IRC ব্যবহারকারীর নাম।", + "realName": "প্রদর্শিত আসল নাম।", + "channels": "যোগ দিতে IRC চ্যানেল।", + "requestCaps": "সংযোগে অনুরোধ করা IRC ক্ষমতার তালিকা।", + "maxBase64FileSizeMiB": "আপলোডের আগে লোকাল ফাইল base64-এ রূপান্তরের জন্য MiB-এ সর্বাধিক আকার। 0 মানে সীমাহীন। শুধুমাত্র লোকাল ফাইলের জন্য প্রযোজ্য, URL আপলোডের জন্য নয়।", + "genericField": "{{field}} কনফিগার করতে ব্যবহৃত।", + "broker": "MQTT ব্রোকার ঠিকানা।", + "mqttAgentId": "এই ইনস্ট্যান্সের জন্য অনন্য শনাক্তকারী, টপিক পাথ তৈরিতে ব্যবহৃত।", + "topicPrefix": "টপিক প্রিফিক্স। ডিফল্ট /picoclaw।", + "mqttUsername": "ব্রোকার প্রমাণীকরণ ব্যবহারকারীর নাম (ঐচ্ছিক)।", + "mqttPassword": "ব্রোকার প্রমাণীকরণ পাসওয়ার্ড (ঐচ্ছিক)।", + "mqttClientId": "MQTT ক্লায়েন্ট ID। স্বয়ংক্রিয়ভাবে তৈরি করতে খালি রাখুন।", + "keepAlive": "সেকেন্ডে Keepalive ব্যবধান। ডিফল্ট 60।", + "qos": "বার্তার সেবার গুণমান স্তর: 0 = সর্বাধিক একবার, 1 = কমপক্ষে একবার, 2 = ঠিক একবার।" + } + }, + "validation": { + "requiredField": "এই ক্ষেত্রটি প্রয়োজনীয়।" + }, + "mqtt": { + "protocolTitle": "প্রোটোকল রেফারেন্স", + "protocolDesc": "ক্লায়েন্টরা নিম্নলিখিত টপিক এবং পেলোড ফর্ম্যাট ব্যবহার করে বার্তা পাঠায় এবং গ্রহণ করে।", + "uplink": "আপলিঙ্ক (ক্লায়েন্ট → এজেন্ট)", + "downlink": "ডাউনলিঙ্ক (এজেন্ট → ক্লায়েন্ট)", + "topicParams": "টপিক প্যারামিটার", + "fieldText": "text", + "uplinkTextDesc": "ব্যবহারকারীর কাছ থেকে প্রাকৃতিক ভাষার নির্দেশনা (প্রয়োজনীয়)।", + "downlinkTextDesc": "এজেন্টের উত্তর টেক্সট। স্ট্রিমিং মোডে, সম্পূর্ণ প্রতিক্রিয়ার জন্য একাধিক বার্তা ক্রমানুসারে যুক্ত করুন।", + "topicPrefixDesc": "টপিক প্রিফিক্স, উপরের কনফিগারেশনের সাথে মেলে।", + "agentIdDesc": "এজেন্ট ID, উপরের কনফিগারেশনের সাথে মেলে।", + "clientIdDesc": "ক্লায়েন্ট-সংজ্ঞায়িত শনাক্তকারী। সুপারিশ: প্রথম চালু হওয়ার সময় একটি UUID তৈরি করুন এবং এটি সংরক্ষণ করুন যাতে একই ডিভাইস সবসময় একই ID ব্যবহার করে।", + "clientIdPlaceholder": "খালি থাকলে স্বয়ংক্রিয়ভাবে তৈরি", + "secretSet": "ইতিমধ্যে কনফিগার করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।", + "secretEmpty": "কনফিগার করা নেই" + } + }, + "pages": { + "agent": { + "load_error": "এজেন্ট সমর্থন তথ্য লোড করতে ব্যর্থ।", + "skills": { + "empty": "বর্তমানে কোনো দক্ষতা উপলব্ধ নেই।", + "install_success": "{{name}} ইনস্টল করা হয়েছে।", + "install_error": "দক্ষতা ইনস্টল করতে ব্যর্থ।", + "search_placeholder": "নাম, বিবরণ বা রেজিস্ট্রি দ্বারা অনুসন্ধান করুন", + "source_label": "প্রকার", + "sort_label": "সাজান", + "import": "দক্ষতা আমদানি করুন", + "import_success": "দক্ষতা আমদানি করা হয়েছে।", + "import_error": "দক্ষতা আমদানি করতে ব্যর্থ।", + "import_invalid_type": "শুধুমাত্র Markdown বা ZIP দক্ষতা ফাইল সমর্থিত।", + "import_invalid_size": "দক্ষতা ফাইল 1 MB বা ছোট হতে হবে।", + "import_constraints": "1 MB পর্যন্ত একটি Markdown বা ZIP দক্ষতা ফাইল আমদানি করুন", + "view": "দেখুন", + "delete": "মুছুন", + "delete_title": "দক্ষতা মুছবেন?", + "delete_description": "\"{{name}}\" ওয়ার্কস্পেস দক্ষতা থেকে সরানো হবে।", + "delete_confirm": "মুছুন", + "delete_success": "দক্ষতা মুছে ফেলা হয়েছে।", + "delete_error": "দক্ষতা মুছতে ব্যর্থ।", + "viewer_title": "দক্ষতার বিষয়বস্তু", + "viewer_description": "এখানে বর্তমানে কার্যকর SKILL.md বিষয়বস্তু পড়ুন।", + "load_detail_error": "দক্ষতার বিষয়বস্তু লোড করতে ব্যর্থ।", + "no_description": "কোনো বিবরণ প্রদান করা হয়নি।", + "no_results": "বর্তমান ফিল্টারের সাথে কোনো দক্ষতা মেলেনি।", + "dropzone_title": "ওয়ার্কস্পেসে আমদানি করুন", + "dropzone_description": "এখানে একটি দক্ষতা ফাইল টেনে আনুন বা ডিস্ক থেকে নির্বাচন করুন।", + "dropzone_label": "এখানে একটি দক্ষতা ফাইল ফেলুন", + "dropzone_active": "এই দক্ষতাটি আমদানি করতে ছাড়ুন", + "dropzone_release": "দক্ষতাটি স্বাভাবিকীকরণ করা হবে এবং ওয়ার্কস্পেস দক্ষতা ডিরেক্টরিতে সংরক্ষণ করা হবে।", + "marketplace_title": "দক্ষতা আবিষ্কার করুন", + "marketplace_description": "দক্ষতা রেজিস্ট্রি অনুসন্ধান করুন এবং এই ওয়ার্কস্পেসে দরকারী দক্ষতা ইনস্টল করুন", + "marketplace_search_placeholder": "github, docker, database এর মতো ক্ষমতার জন্য অনুসন্ধান করুন...", + "marketplace_search_action": "অনুসন্ধান", + "marketplace_search_status": "অনুসন্ধান স্ট্যাটাস", + "marketplace_install_status": "ইনস্টল স্ট্যাটাস", + "marketplace_notice_title": "নিরাপত্তা বিজ্ঞপ্তি", + "marketplace_notice_body": "রেজিস্ট্রি দক্ষতা তৃতীয় পক্ষের বিষয়বস্তু। ইনস্টল করার আগে লেখক, পৃষ্ঠার URL, নির্দেশাবলী এবং প্রয়োজনীয় কোড বা ক্রেডেনশিয়াল পর্যালোচনা করুন।", + "marketplace_status_disabled": "নিষ্ক্রিয়। প্রথমে টুল পৃষ্ঠায় সংশ্লিষ্ট টুল সক্ষম করুন।", + "marketplace_status_enable_hint": "প্রথমে টুল পৃষ্ঠায় সম্পর্কিত টুল সক্ষম করুন।", + "marketplace_search_error": "রেজিস্ট্রি অনুসন্ধান করতে ব্যর্থ।", + "marketplace_loading_results": "দক্ষতা অনুসন্ধান করা হচ্ছে...", + "marketplace_loading_more": "আরও দক্ষতা লোড করা হচ্ছে...", + "marketplace_results_title": "“{{query}}”-এর জন্য {{count}}টি ফলাফল", + "marketplace_results_hint": "রেজিস্ট্রি ফলাফল বর্তমান ওয়ার্কস্পেসে ইনস্টল হয়।", + "marketplace_install_action": "ইনস্টল", + "marketplace_installed": "ইনস্টল করা হয়েছে", + "marketplace_view_installed": "লোকাল দেখুন", + "marketplace_installed_hint": "“{{name}}” হিসেবে এই ওয়ার্কস্পেসে ইতিমধ্যে উপলব্ধ।", + "marketplace_empty_results": "“{{query}}”-এর সাথে কোনো ইনস্টলযোগ্য দক্ষতা মেলেনি।", + "marketplace_idle": "কনফিগার করা রেজিস্ট্রি থেকে ইনস্টলযোগ্য দক্ষতা আবিষ্কার করতে একটি ক্ষমতার জন্য অনুসন্ধান করুন।", + "marketplace_unavailable": "রেজিস্ট্রি অনুসন্ধান বর্তমানে অনুপলব্ধ। দক্ষতা টুল কনফিগারেশন পরীক্ষা করুন।", + "sort": { + "name_asc": "নাম (A-Z)", + "name_desc": "নাম (Z-A)", + "source": "প্রকার" + }, + "origin": { + "all": "সব প্রকার", + "builtin": "বিল্টইন", + "third_party": "তৃতীয় পক্ষ", + "manual": "ম্যানুয়াল" + }, + "summary": { + "total": "মোট দক্ষতা" + }, + "detail_tabs": { + "preview": "প্রিভিউ", + "raw": "র", + "meta": "মেটাডেটা" + }, + "metadata": { + "name": "নাম", + "description": "বিবরণ", + "registry": "রেজিস্ট্রি", + "url": "লিঙ্ক ঠিকানা", + "version": "ইনস্টল করা সংস্করণ", + "lines": "লাইন সংখ্যা", + "characters": "অক্ষর সংখ্যা" + }, + "marketplace_installDisabled": { + "installing": "ইনস্টল করা হচ্ছে...", + "installed": "ইতিমধ্যে ইনস্টল করা হয়েছে", + "cannotInstall": "ইনস্টল করা যাবে না: সম্পর্কিত টুল সক্ষম নয়" + } + }, + "tools": { + "search_placeholder": "টুল অনুসন্ধান করুন...", + "no_results": "আপনার মানদণ্ডের সাথে কোনো টুল মেলেনি।", + "filter": { + "all": "সব স্ট্যাটাস", + "enabled": "সক্ষম", + "disabled": "নিষ্ক্রিয়", + "blocked": "ব্লক করা" + }, + "empty": "কোনো টুল উপলব্ধ নেই।", + "enable_success": "টুল সক্ষম করা হয়েছে।", + "disable_success": "টুল নিষ্ক্রিয় করা হয়েছে।", + "toggle_error": "টুলের অবস্থা আপডেট করতে ব্যর্থ।", + "library_title": "টুল লাইব্রেরি", + "library_description": "আপনার AI এজেন্টদের জন্য উপলব্ধ টুলসেট ব্রাউজ এবং পরিচালনা করুন।", + "web_search": { + "title": "ওয়েব অনুসন্ধান", + "description": "এজেন্টদের সর্বশেষ বাস্তব-বিশ্বের তথ্য খুঁজে পেতে ওয়েব অনুসন্ধান ক্ষমতা প্রদান করুন। স্বয়ংক্রিয়ভাবে সর্বোত্তম সক্রিয় প্রোভাইডারে রুট করে।", + "unsaved_prompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। ওয়েব অনুসন্ধান কনফিগারেশনে লিখতে সংরক্ষণ করুন।", + "global_settings": "সাধারণ", + "providers_config": "ইন্টিগ্রেশন", + "load_error": "ওয়েব অনুসন্ধান কনফিগারেশন লোড করতে ব্যর্থ।", + "save": "পরিবর্তন সংরক্ষণ করুন", + "open_settings": "সেটিংস খুলুন", + "save_success": "সেটিংস সফলভাবে সংরক্ষিত হয়েছে।", + "save_error": "সেটিংস সংরক্ষণ করতে ব্যর্থ।", + "provider": "প্রাথমিক প্রোভাইডার", + "provider_description": "ওয়েব অনুসন্ধান টুল একটি অনুরোধ পরিচালনা করার সময় ব্যবহার করার জন্য ডিফল্ট প্রোভাইডার নির্বাচন করুন।", + "proxy": "HTTPS প্রক্সি", + "proxy_description": "অন্তর্নিহিত ওয়েব অনুরোধের জন্য ঐচ্ছিক বৈশ্বিক HTTP/S প্রক্সি।", + "prefer_native": "নেটিভ অনুসন্ধান পছন্দ করুন", + "prefer_native_hint": "সক্ষম থাকলে, মডেলটি কনফিগার করা প্রোভাইডার তালিকার পরিবর্তে তার বিল্ট-ইন অনুসন্ধান ক্ষমতা ব্যবহার করতে পারে।", + "provider_hint": "এই প্রোভাইডারকে সক্ষম করুন এবং কোনো প্রয়োজনীয় সংযোগ সেটিংস পূরণ করুন।", + "max_results": "সর্বাধিক ফলাফল", + "base_url": "বেস URL", + "base_url_placeholder": "ঐচ্ছিক এন্ডপয়েন্ট ওভাররাইড", + "api_key": "API কী / টোকেন", + "api_key_placeholder": "API কী লিখুন, আসল কী রাখতে এটি খালি রাখুন", + "none": "অনুপলব্ধ" + }, + "status": { + "enabled": "সক্ষম", + "disabled": "নিষ্ক্রিয়", + "blocked": "ব্লক করা" + }, + "categories": { + "automation": "অটোমেশন", + "filesystem": "ফাইলসিস্টেম", + "web": "ওয়েব", + "communication": "যোগাযোগ", + "skills": "দক্ষতা", + "agents": "এজেন্ট", + "hardware": "হার্ডওয়্যার", + "discovery": "আবিষ্কার" + }, + "reasons": { + "requires_linux": "এই টুল শুধুমাত্র Linux হোস্টে কাজ করে যেখানে প্রয়োজনীয় ডিভাইস ফাইল উন্মুক্ত আছে।", + "requires_serial_platform": "এই টুল বর্তমানে অ্যাক্সেসযোগ্য সিরিয়াল পোর্ট সহ Linux, macOS এবং Windows হোস্ট সমর্থন করে।", + "requires_skills": "এই দক্ষতা-রেজিস্ট্রি টুল ব্যবহার করার আগে `tools.skills` সক্ষম করুন।", + "requires_subagent": "স্প্যান টুল কাজ অর্পণ করার আগে `tools.subagent` সক্ষম করুন।", + "requires_mcp_discovery": "MCP আবিষ্কার টুল উপলব্ধ হওয়ার আগে `tools.mcp.discovery` সক্ষম করুন।", + "requires_web_search_provider": "কমপক্ষে একটি প্রস্তুত বাহ্যিক ওয়েব-অনুসন্ধান প্রোভাইডার কনফিগার করুন।" + } + } + }, + "config": { + "load_error": "কনফিগারেশন লোড করতে ব্যর্থ। অনুগ্রহ করে রিফ্রেশ করুন এবং আবার চেষ্টা করুন।", + "workspace": "ওয়ার্কস্পেস ডিরেক্টরি", + "workspace_hint": "এজেন্ট ফাইল অপারেশনের জন্য বেস ডিরেক্টরি।", + "restrict_workspace": "ওয়ার্কস্পেসে সীমাবদ্ধ করুন", + "restrict_workspace_hint": "শুধুমাত্র ওয়ার্কস্পেসের ভিতরে ফাইল অপারেশন অনুমতি দিন।", + "split_on_marker": "চ্যাটি মোড", + "split_on_marker_hint": "বাস্তব মানুষের চ্যাটিংয়ের মতো দীর্ঘ বার্তাগুলিকে ছোট বার্তায় বিভক্ত করুন।", + "tool_feedback_enabled": "টুল ফিডব্যাক", + "tool_feedback_enabled_hint": "প্রতিটি টুল চালানোর আগে বর্তমান চ্যাটে একটি সংক্ষিপ্ত নির্বাহ নোট পাঠান।", + "tool_feedback_separate_messages": "পৃথক ফিডব্যাক বার্তা", + "tool_feedback_separate_messages_hint": "একটি একক প্লেসহোল্ডার/প্রগ্রেস বার্তা পুনরায় ব্যবহার করার পরিবর্তে প্রতিটি টুল ফিডব্যাক আপডেটকে তার নিজস্ব চ্যাট বার্তা হিসেবে রাখুন।", + "tool_feedback_max_args_length": "টুল আর্গস প্রিভিউ দৈর্ঘ্য", + "tool_feedback_max_args_length_hint": "প্রতিটি টুল আর্গুমেন্ট প্রিভিউতে দেখানো অক্ষরের সর্বাধিক সংখ্যা। ডিফল্ট ব্যবহার করতে 0 সেট করুন।", + "exec_enabled": "কমান্ড অনুমতি দিন", + "exec_enabled_hint": "অ্যাপের জন্য কমান্ড নির্বাহ সক্ষম বা নিষ্ক্রিয় করুন। নিষ্ক্রিয় থাকলে, কোনো কমান্ড অনুরোধ চলবে না।", + "allow_remote": "রিমোট কমান্ড অনুমতি দিন", + "allow_remote_hint": "সক্ষম থাকলে, রিমোট সেশন বা নন-লোকাল প্রসঙ্গও কমান্ড চালাতে পারে। নিষ্ক্রিয় থাকলে, কমান্ড নির্বাহ স্থানীয় নিরাপদ প্রসঙ্গে সীমাবদ্ধ থাকে।", + "enable_deny_patterns": "ব্ল্যাকলিস্ট সক্ষম করুন", + "enable_deny_patterns_hint": "সক্ষম থাকলে, অ্যাপ্লিকেশন তার বিল্ট-ইন বিপজ্জনক প্যাটার্ন এবং নিচের কাস্টম কমান্ড ব্ল্যাকলিস্টের সাথে মিলে যাওয়া কমান্ডগুলি ব্লক করে।", + "exec_timeout_seconds": "কমান্ড টাইমআউট (সেকেন্ড)", + "exec_timeout_seconds_hint": "কমান্ড অনুরোধের জন্য সর্বাধিক রানটাইম। ডিফল্ট টাইমআউট ব্যবহার করতে 0 সেট করুন।", + "custom_deny_patterns": "কমান্ড ব্ল্যাকলিস্ট", + "custom_deny_patterns_hint": "অতিরিক্ত কমান্ড-ব্লকিং নিয়ম যোগ করুন, প্রতি লাইনে একটি রেগুলার এক্সপ্রেশন। এখানের কোনো নিয়মের সাথে মেলে এমন একটি কমান্ড ব্লক করা হবে।", + "custom_allow_patterns": "কমান্ড হোয়াইটলিস্ট", + "custom_allow_patterns_hint": "অতিরিক্ত কমান্ড-অনুমতি নিয়ম যোগ করুন, প্রতি লাইনে একটি রেগুলার এক্সপ্রেশন। এখানের কোনো নিয়মের সাথে মেলে এমন একটি কমান্ড ব্ল্যাকলিস্ট মিল এড়িয়ে যায়, তবে অন্যান্য নিরাপত্তা সীমা এখনও প্রযোজ্য।", + "custom_patterns_placeholder": "^rm\\s+-rf\\b\n^git\\s+push\\b", + "pattern_detector_title": "প্যাটার্ন সনাক্তকরণ টুল", + "pattern_detector_hint": "যেকোনো ব্ল্যাকলিস্ট বা হোয়াইটলিস্ট প্যাটার্নের সাথে মেলে কিনা তা পরীক্ষা করতে একটি কমান্ড লিখুন।", + "pattern_detector_input_placeholder": "পরীক্ষার জন্য একটি কমান্ড লিখুন, যেমন rm -rf /tmp", + "pattern_detector_test_button": "পরীক্ষা", + "pattern_detector_result_allowed": "অনুমোদিত (হোয়াইটলিস্টের সাথে মেলে)", + "pattern_detector_result_blocked": "ব্লক করা (ব্ল্যাকলিস্টের সাথে মেলে)", + "pattern_detector_result_no_match": "কোনো মিল নেই (ডিফল্ট নিয়ম ব্যবহার করবে)", + "allow_shell_execution": "সময়সূচী কমান্ড অনুমতি দিন", + "allow_shell_execution_hint": "ডিফল্টরূপে সময়সূচী কাজগুলিকে কমান্ড চালানোর অনুমতি দিন। নিষ্ক্রিয় থাকলে, ব্যবহারকারীদের একটি কমান্ড কাজের সময়সূচী করতে command_confirm=true পাস করতে হবে।", + "cron_exec_timeout": "সময়সূচী কমান্ড টাইমআউট (মিনিট)", + "cron_exec_timeout_hint": "সময়সূচী কমান্ডের জন্য সর্বাধিক রানটাইম। টাইমআউট নিষ্ক্রিয় করতে 0 সেট করুন।", + "max_tokens": "ম্যাক্স টোকেন", + "max_tokens_hint": "প্রতি মডেল প্রতিক্রিয়ার জন্য উপরের টোকেন সীমা।", + "context_window": "প্রসঙ্গ উইন্ডো", + "context_window_hint": "টোকেনে মডেল ইনপুট প্রসঙ্গ ধারণক্ষমতা। ডিফল্ট ব্যবহার করতে খালি রাখুন (4x ম্যাক্স টোকেন)।", + "max_tool_iterations": "ম্যাক্স টুল ইটারেশন", + "max_tool_iterations_hint": "একটি একক কাজের মধ্যে সর্বাধিক টুল-কল লুপ।", + "summarize_threshold": "সংক্ষিপ্তসারের বার্তা থ্রেশহোল্ড", + "summarize_threshold_hint": "এই সংখ্যক বার্তার পরে সংক্ষিপ্তসার শুরু করুন।", + "summarize_token_percent": "সংক্ষিপ্তসারের টোকেন শতাংশ", + "summarize_token_percent_hint": "কথোপকথন সংক্ষিপ্তসার ট্রিগার হলে ব্যবহৃত।", + "turn_profile": "অনুরোধ প্রসঙ্গ নীতি", + "turn_profile_hint": "প্রতিটি অনুরোধে কী প্রসঙ্গ বহন করে তা নিয়ন্ত্রণ করে। স্বাভাবিক চ্যাট আচরণ রাখতে নিষ্ক্রিয় রাখুন।", + "turn_profile_enabled": "নীতি সক্ষম করুন", + "turn_profile_enabled_hint": "সক্ষম থাকলে, এই নীতি প্রতিটি নতুন টার্নে প্রযোজ্য। নিষ্ক্রিয় থাকলে, PicoClaw মূল প্রসঙ্গ আচরণ ব্যবহার করে।", + "turn_profile_mode_default": "ডিফল্ট", + "turn_profile_mode_off": "বন্ধ", + "turn_profile_mode_custom": "অনুমোদিত তালিকা", + "turn_profile_history": "ইতিহাস প্রসঙ্গ", + "turn_profile_history_hint": "ডিফল্ট এই সেশনের পূর্ববর্তী বার্তা অন্তর্ভুক্ত করে। বন্ধ করলে টার্নটি একটি নতুন চ্যাটের মতো আচরণ করে এবং তার ফলাফল ইতিহাসে সংরক্ষণ করা এড়িয়ে যায়।", + "turn_profile_system_prompt": "সিস্টেম প্রসঙ্গ", + "turn_profile_system_prompt_hint": "ডিফল্ট PicoClaw পরিচয়, ওয়ার্কস্পেস, মেমরি এবং রানটাইম নির্দেশাবলী অন্তর্ভুক্ত করে। বন্ধ করলে অনুরোধ দ্বারা স্পষ্টভাবে সরবরাহিত সিস্টেম প্রম্পটগুলিই রাখা হয়।", + "turn_profile_skills": "দক্ষতা প্রম্পট", + "turn_profile_skills_hint": "ডিফল্ট উপলব্ধ দক্ষতা এবং সক্রিয় দক্ষতা নির্দেশাবলী অন্তর্ভুক্ত করে। বন্ধ করলে সেগুলি লুকায়। অনুমোদিত তালিকা প্রতি লাইনে এক করে প্রবেশ করানো দক্ষতার নামগুলিই রাখে।", + "turn_profile_skills_allow_placeholder": "skill-name\nanother-skill", + "turn_profile_tools": "কলযোগ্য টুল", + "turn_profile_tools_hint": "ডিফল্ট স্বাভাবিক টুল উন্মুক্ত করে। বন্ধ টুল কলগুলি প্রতিরোধ করে। অনুমোদিত তালিকা প্রতি লাইনে এক করে প্রবেশ করানো টুলের নামগুলিই রাখে, যেমন web_search।", + "turn_profile_tools_allow_placeholder": "web_search\nweb_fetch", + "session_scope": "সেশন স্কোপ", + "session_scope_hint": "পিয়ার/চ্যানেল জুড়ে চ্যাট প্রসঙ্গ কীভাবে বিচ্ছিন্ন করা হয়।", + "session_scope_per_channel_peer": "প্রতি চ্যানেল + পিয়ার", + "session_scope_per_channel_peer_desc": "প্রতিটি চ্যানেলে প্রতিটি ব্যবহারকারীর জন্য আলাদা প্রসঙ্গ।", + "session_scope_per_channel": "প্রতি চ্যানেল", + "session_scope_per_channel_desc": "প্রতি চ্যানেলে একটি শেয়ার করা প্রসঙ্গ।", + "session_scope_per_peer": "প্রতি পিয়ার", + "session_scope_per_peer_desc": "চ্যানেল জুড়ে প্রতি ব্যবহারকারীর জন্য একটি প্রসঙ্গ।", + "session_scope_global": "গ্লোবাল", + "session_scope_global_desc": "সব বার্তা একটি গ্লোবাল প্রসঙ্গ শেয়ার করে।", + "heartbeat_enabled": "হার্টবিট", + "heartbeat_enabled_hint": "পর্যায়ক্রমিক হার্টবিট বার্তা পাঠান।", + "heartbeat_interval": "হার্টবিট ব্যবধান (মিনিট)", + "heartbeat_interval_hint": "হার্টবিট সংকেতের মধ্যে মিনিটে ব্যবধান।", + "devices_enabled": "ডিভাইস সক্ষম করুন", + "devices_enabled_hint": "হার্ডওয়্যার-ডিভাইস ইন্টিগ্রেশন সক্ষম করুন।", + "monitor_usb": "USB মনিটর করুন", + "monitor_usb_hint": "ডিভাইস সক্ষম থাকলে USB প্লাগ/আনপ্লাগ ইভেন্ট দেখুন।", + "autostart_label": "লগইনে চালু করুন", + "autostart_hint": "লগ ইন করার সময় স্বয়ংক্রিয়ভাবে PicoClaw Web চালু করুন।", + "autostart_unsupported": "এই প্ল্যাটফর্মে লগইনে চালু করা সমর্থিত নয়।", + "autostart_load_error": "লগইনে চালু করার স্ট্যাটাস লোড করতে ব্যর্থ।", + "server_port": "সার্ভিস পোর্ট", + "server_port_hint": "PicoClaw Web দ্বারা ব্যবহৃত HTTP পোর্ট।", + "launcher_section_hint": "এই বিভাগের পরিবর্তন লঞ্চার পুনরায় চালু হওয়ার পরে কার্যকর হয়।", + "gateway_restart_hint": "এই বিভাগের পরিবর্তন গেটওয়ে পুনরায় চালু হওয়ার পরে কার্যকর হয়।", + "dashboard_password": "লগইন পাসওয়ার্ড", + "dashboard_password_hint": "একটি নতুন লগইন পাসওয়ার্ড সেট করুন।", + "dashboard_password_placeholder": "কমপক্ষে ৮ অক্ষর", + "dashboard_password_confirm": "নতুন পাসওয়ার্ড নিশ্চিত করুন", + "dashboard_password_confirm_hint": "নতুন লগইন পাসওয়ার্ড আবার লিখুন।", + "dashboard_password_confirm_placeholder": "পাসওয়ার্ড আবার লিখুন", + "dashboard_password_required": "নতুন লগইন পাসওয়ার্ড লিখুন এবং নিশ্চিত করুন।", + "dashboard_password_mismatch": "লগইন পাসওয়ার্ড মিলছে না।", + "dashboard_password_min_length": "লগইন পাসওয়ার্ড কমপক্ষে ৮ অক্ষরের হতে হবে।", + "lan_access": "LAN অ্যাক্সেস সক্ষম করুন", + "lan_access_hint": "আপনার স্থানীয় নেটওয়ার্কের অন্যান্য ডিভাইস থেকে অ্যাক্সেসের অনুমতি দিন।", + "allowed_cidrs": "অনুমোদিত নেটওয়ার্ক CIDR", + "allowed_cidrs_hint": "শুধুমাত্র এই CIDR পরিসরের ক্লায়েন্টরা সার্ভিস অ্যাক্সেস করতে পারে। প্রতি লাইনে একটি বা কমা দিয়ে আলাদা। সবাইকে অনুমতি দিতে খালি রাখুন।", + "allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8", + "evolution_section_hint": "এজেন্টকে সম্পন্ন টার্ন থেকে শিখতে দিন এবং দক্ষতার উন্নতি প্রস্তুত করুন।", + "evolution_enabled": "ইভোলিউশন সক্ষম করুন", + "evolution_enabled_hint": "সম্পন্ন টার্নের জন্য শেখার ডেটা রেকর্ড করুন। ড্রাফ্ট এবং অ্যাপ্লাই মোড দক্ষতা আপডেটও তৈরি করতে পারে।", + "evolution_mode": "ইভোলিউশন মোড", + "evolution_mode_hint": "Observe শুধু ডেটা রেকর্ড করে, Draft প্রার্থী দক্ষতা প্রস্তুত করে, Apply ওয়ার্কস্পেস দক্ষতায় গৃহীত ড্রাফ্ট লিখতে পারে।", + "evolution_mode_observe": "পর্যবেক্ষণ", + "evolution_mode_draft": "ড্রাফ্ট", + "evolution_mode_apply": "প্রয়োগ", + "evolution_state_dir": "স্টেট ডিরেক্টরি", + "evolution_state_dir_hint": "ইভোলিউশন স্টেটের জন্য ঐচ্ছিক ডিরেক্টরি। ওয়ার্কস্পেস ডিফল্ট ব্যবহার করতে খালি রাখুন।", + "evolution_min_task_count": "ন্যূনতম কাজ গণনা", + "evolution_min_task_count_hint": "একটি প্যাটার্ন একটি ড্রাফ্ট তৈরি করার আগে প্রয়োজনীয় ন্যূনতম সম্পর্কিত কাজ।", + "evolution_min_success_ratio": "ন্যূনতম সাফল্যের অনুপাত", + "evolution_min_success_ratio_hint": "ক্লাস্টার্ড কাজের জন্য প্রয়োজনীয় সাফল্যের অনুপাত। 0 এর চেয়ে বড় এবং 1 পর্যন্ত একটি মান ব্যবহার করুন।", + "evolution_cold_path_trigger": "কোল্ড পাথ ট্রিগার", + "evolution_cold_path_trigger_hint": "যোগ্য শেখার রেকর্ডের জন্য ড্রাফ্ট জেনারেশন কখন চলবে তা চয়ন করুন।", + "evolution_cold_path_after_turn": "প্রতিটি টার্নের পরে", + "evolution_cold_path_scheduled": "সময়সূচী", + "evolution_cold_path_manual": "বন্ধ", + "evolution_cold_path_times": "সময়সূচী সময়", + "evolution_cold_path_times_hint": "সময়সূচী কোল্ড-পাথ প্রক্রিয়াকরণের জন্য চালানোর সময়। প্রতি লাইনে একটি HH:MM মান লিখুন।", + "mcp_section_hint": "ম্যানুয়ালি config.json সম্পাদনা না করে MCP সার্ভার কনফিগার করুন।", + "mcp_enabled": "MCP সক্ষম করুন", + "mcp_enabled_hint": "MCP সার্ভার ইন্টিগ্রেশন চালু বা বন্ধ করুন।", + "mcp_discovery_enabled": "MCP আবিষ্কার সক্ষম করুন", + "mcp_discovery_enabled_hint": "MCP আবিষ্কার টুলকে নিবন্ধিত MCP সার্ভার অনুসন্ধান করার অনুমতি দিন।", + "mcp_discovery_ttl": "আবিষ্কৃত টুল আনলক TTL", + "mcp_discovery_ttl_hint": "অনুসন্ধানের পরে আবিষ্কৃত টুলগুলি কত টুল-নির্বাহ TTL টিক উপলব্ধ থাকবে।", + "mcp_discovery_max_results": "আবিষ্কার সর্বাধিক ফলাফল", + "mcp_discovery_max_results_hint": "প্রতি কোয়েরিতে ফিরিয়ে দেওয়া সর্বাধিক MCP আবিষ্কার মিল।", + "mcp_discovery_use_bm25": "BM25 র‍্যাঙ্কিং ব্যবহার করুন", + "mcp_discovery_use_bm25_hint": "MCP আবিষ্কার ফলাফলের জন্য BM25 লেক্সিকাল স্কোরিং ব্যবহার করুন।", + "mcp_discovery_use_regex": "রেগেক্স অনুসন্ধান সক্ষম করুন", + "mcp_discovery_use_regex_hint": "MCP আবিষ্কারে রেগেক্স-ভিত্তিক মিলের অনুমতি দিন।", + "mcp_servers": "MCP সার্ভার", + "mcp_servers_hint": "MCP সার্ভার যোগ, সম্পাদনা বা সরান।", + "mcp_server_new": "নতুন MCP সার্ভার", + "mcp_server_add": "সার্ভার যোগ করুন", + "mcp_server_remove": "সরান", + "mcp_server_enabled": "সক্ষম", + "mcp_server_discovery_mode": "আবিষ্কার মোড", + "mcp_server_discovery_mode_inherit": "গ্লোবাল আবিষ্কার মোড অনুসরণ করুন", + "mcp_server_discovery_mode_deferred": "বিলম্বিত আবিষ্কার", + "mcp_server_discovery_mode_eager": "ইগার নিবন্ধন", + "mcp_server_name_placeholder": "সার্ভারের নাম (যেমন github)", + "mcp_server_url_placeholder": "সার্ভার URL (যেমন https://example.com/mcp)", + "mcp_server_command_placeholder": "কমান্ড (যেমন npx)", + "mcp_server_env_file_placeholder": "পরিবেশ ফাইল পাথ (ঐচ্ছিক)", + "mcp_server_args_placeholder": "আর্গস, প্রতি লাইনে একটি", + "mcp_server_env_placeholder": "পরিবেশ JSON অবজেক্ট", + "mcp_server_headers_placeholder": "হেডার JSON অবজেক্ট", + "sections": { + "agent": "এজেন্ট", + "runtime": "রানটাইম", + "evolution": "ইভোলিউশন", + "mcp": "MCP", + "exec": "কমান্ড চালান", + "cron": "ক্রন কাজ", + "launcher": "লঞ্চার", + "devices": "ডিভাইস" + }, + "open_raw": "র কনফিগ", + "back_to_visual": "ভিজ্যুয়াল কনফিগ", + "raw_json_title": "র JSON কনফিগারেশন", + "json_placeholder": "বৈধ JSON কনফিগারেশন লিখুন...", + "save_success": "কনফিগারেশন সফলভাবে সংরক্ষিত হয়েছে।", + "save_error": "কনফিগারেশন সংরক্ষণ করতে ব্যর্থ।", + "reset_confirm_title": "পরিবর্তন রিসেট করুন", + "reset_confirm_desc": "আপনি কি আপনার অসংরক্ষিত পরিবর্তনগুলি সর্বশেষ সংরক্ষিত অবস্থায় রিসেট করতে চান?", + "reset_success": "পরিবর্তনগুলি সর্বশেষ সংরক্ষিত অবস্থায় রিসেট করা হয়েছে।", + "invalid_json": "অবৈধ JSON ফর্ম্যাট।", + "format_success": "JSON সফলভাবে ফর্ম্যাট করা হয়েছে।", + "format_error": "অবৈধ JSON ফর্ম্যাট।", + "format": "ফর্ম্যাট", + "unsaved_changes": "আপনার অসংরক্ষিত পরিবর্তন রয়েছে।", + "factory_reset": "ফ্যাক্টরি রিসেট", + "factory_reset_confirm_title": "ফ্যাক্টরি ডিফল্টে রিসেট করুন", + "factory_reset_confirm_desc": "এটি সমস্ত কনফিগারেশন ফ্যাক্টরি ডিফল্টে রিসেট করবে। API কী এবং নিরাপত্তা ক্রেডেনশিয়াল সংরক্ষিত থাকবে। বর্তমান কনফিগারেশনের একটি ব্যাকআপ তৈরি করা হবে।", + "factory_reset_confirm": "ডিফল্টে রিসেট করুন", + "factory_reset_success": "কনফিগারেশন ফ্যাক্টরি ডিফল্টে রিসেট করা হয়েছে।", + "factory_reset_error": "কনফিগারেশন রিসেট করতে ব্যর্থ।" + }, + "logs": { + "log_level_error": "লগ লেভেল আপডেট করতে ব্যর্থ।", + "clear": "লগ পরিষ্কার করুন", + "empty": "লগের জন্য অপেক্ষা করা হচ্ছে..." + } + }, + "tour": { + "skip": "ট্যুর এড়িয়ে যান", + "prev": "পূর্ববর্তী", + "next": "পরবর্তী", + "finish": "শেষ করুন", + "welcome": { + "title": "PicoClaw-এ স্বাগতম", + "description": "PicoClaw একটি শক্তিশালী AI সহকারী প্ল্যাটফর্ম। মৌলিক সেটআপ সম্পন্ন করতে আপনাকে সাহায্য করতে কয়েক সেকেন্ড সময় নিই।" + }, + "models": { + "title": "মডেল কনফিগার করুন", + "description": "AI প্রোভাইডারদের জন্য API কী কনফিগার করতে বাঁ দিকের \"মডেল\" মেনুতে ক্লিক করুন। শুধুমাত্র কনফিগার করা মডেলগুলি চ্যাটের জন্য ব্যবহার করা যেতে পারে।" + }, + "gateway": { + "title": "গেটওয়ে চালু করুন", + "description": "মডেল কনফিগার করার পরে, AI-এর সাথে চ্যাট শুরু করতে উপরের \"গেটওয়ে চালু করুন\" বোতামে ক্লিক করুন।" + }, + "docs": { + "title": "ডকুমেন্টেশন দেখুন", + "description": "আরও সাহায্যের প্রয়োজন? বিস্তারিত গাইড এবং কনফিগারেশন ডকুমেন্ট দেখতে উপরের ডান কোণে ডকুমেন্টেশন বোতামে ক্লিক করুন।" + } + } +}