Merge remote-tracking branch 'upstream/main'

This commit is contained in:
afjcjsbx
2026-05-30 20:29:53 +02:00
22 changed files with 1608 additions and 87 deletions
+3 -1
View File
@@ -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`.
<details>
<summary><b>Local deployment (Ollama, vLLM, etc.)</b></summary>
+7
View File
@@ -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
+19
View File
@@ -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=
+56
View File
@@ -0,0 +1,56 @@
//go:build azidentity
// 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),
)
}
+24
View File
@@ -0,0 +1,24 @@
//go:build !azidentity
// 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)
}
+39
View File
@@ -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)
}
}
+45 -5
View File
@@ -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 != "" {
+67
View File
@@ -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)
}
}
+17 -8
View File
@@ -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.)
@@ -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")
}
}
+5 -2
View File
@@ -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)
}
}
@@ -291,6 +291,9 @@ export function AppHeader() {
<DropdownMenuItem onClick={() => i18n.changeLanguage("pt-BR")}>
Português (Brasil)
</DropdownMenuItem>
<DropdownMenuItem onClick={() => i18n.changeLanguage("bn-IN")}>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => i18n.changeLanguage("zh")}>
</DropdownMenuItem>
@@ -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<HTMLTextAreaElement>) => void
onDragEnter: (event: ReactDragEvent<HTMLDivElement>) => void
onDragLeave: (event: ReactDragEvent<HTMLDivElement>) => void
onDragOver: (event: ReactDragEvent<HTMLDivElement>) => void
onDrop: (event: ReactDragEvent<HTMLDivElement>) => 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 (
<div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] [scrollbar-gutter:stable] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48">
<div className="bg-card border-border/60 pointer-events-auto relative mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-sm">
<div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 [scrollbar-gutter:stable] overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48">
<div
className={cn(
"bg-card border-border/60 pointer-events-auto relative mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-sm transition-colors",
isDragActive && "border-violet-400/70 bg-violet-500/5",
)}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
>
{isDragActive && (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-violet-400/70 bg-violet-500/10">
<div className="bg-background/95 text-foreground rounded-full px-4 py-2 text-sm font-medium shadow-sm">
{t("chat.dropImagesActive")}
</div>
</div>
)}
{attachments.length > 0 && (
<div className="mb-3 flex flex-wrap gap-2 px-2">
{attachments.map((attachment, index) => (
@@ -115,6 +149,7 @@ export function ChatComposer({
onCompositionEnd={() => {
composingRef.current = false
}}
onPaste={onPaste}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={!canInput}
+103 -63
View File
@@ -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<string> {
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<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const dragDepthRef = useRef(0)
const [isAtBottom, setIsAtBottom] = useState(true)
const [hasScrolled, setHasScrolled] = useState(false)
const [input, setInput] = useState("")
const [attachments, setAttachments] = useState<ChatAttachment[]>([])
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<HTMLInputElement>) => {
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<HTMLTextAreaElement>,
) => {
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<HTMLDivElement>) => {
if (!hasFileTransfer(event.dataTransfer)) {
return
}
event.preventDefault()
if (!canInput) {
return
}
dragDepthRef.current += 1
setIsDragActive(true)
}
const handleComposerDragLeave = (event: DragEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
if (!hasFileTransfer(event.dataTransfer)) {
return
}
event.preventDefault()
event.dataTransfer.dropEffect = canInput ? "copy" : "none"
}
const handleComposerDrop = async (event: DragEvent<HTMLDivElement>) => {
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() {
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp,image/bmp"
accept={CHAT_IMAGE_ACCEPT}
multiple
className="hidden"
onChange={handleImageSelection}
/>
@@ -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}
/>
</div>
@@ -39,7 +39,7 @@ export function UserMessage({
<img
key={`${attachment.url}-${index}`}
src={attachment.url}
alt={attachment.filename || "Uploaded image"}
alt={attachment.filename || t("chat.uploadedImage")}
className="max-h-72 max-w-full object-cover"
/>
))}
@@ -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<string>(CHAT_IMAGE_MIME_TYPES)
const CHAT_IMAGE_EXTENSION_BY_MIME: Record<string, string> = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
}
const CHAT_IMAGE_MIME_BY_EXTENSION: Record<string, string> = {
".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<string> {
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<ChatAttachment[]> {
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
}
+7
View File
@@ -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 {
+960
View File
@@ -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": "আরও সাহায্যের প্রয়োজন? বিস্তারিত গাইড এবং কনফিগারেশন ডকুমেন্ট দেখতে উপরের ডান কোণে ডকুমেন্টেশন বোতামে ক্লিক করুন।"
}
}
}
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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}}\".",
+2 -1
View File
@@ -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}}\".",
+2 -1
View File
@@ -97,8 +97,9 @@
"contextTitle": "上下文",
"contextDetail": "查看详情",
"attachImage": "添加图片",
"dropImagesActive": "松开以添加图片",
"removeImage": "移除图片",
"uploadedImage": "已上传图片",
"uploadedImage": "已添加图片",
"invalidImage": "“{{name}}”不是支持的图片文件。",
"imageTooLarge": "“{{name}}”超过了 {{size}} 限制。",
"imageReadFailed": "读取“{{name}}”失败。",