From c286b9e24da8fb43783e882a70a1e15b78f1b5b6 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Mon, 20 Jul 2026 15:32:49 +0530 Subject: [PATCH 1/2] UI --- apps/desktop-ui/src/app/app/api-keys/page.tsx | 22 +- .../src/components/api-client/api-client.tsx | 32 +- .../collections/collections-sidebar.tsx | 21 +- .../components/api-client/import-dialog.tsx | 33 +- .../components/api-client/response-panel.tsx | 4 + .../src/lib/__tests__/import-export.test.ts | 125 +++++++- .../lib/__tests__/secret-variables.test.ts | 58 ++++ .../desktop-ui/src/lib/api-key-vault-utils.ts | 20 ++ apps/desktop-ui/src/lib/desktop/api-fetch.ts | 4 +- apps/desktop-ui/src/lib/import/postman.ts | 80 ++++- apps/desktop-ui/src/lib/secret-variables.ts | 124 ++++++++ .../desktop/src-tauri/src/http/mock_server.rs | 286 ++++++++++++++++++ apps/desktop/src-tauri/src/http/mod.rs | 1 + apps/desktop/src-tauri/src/lib.rs | 6 + 14 files changed, 769 insertions(+), 47 deletions(-) create mode 100644 apps/desktop-ui/src/lib/__tests__/secret-variables.test.ts create mode 100644 apps/desktop-ui/src/lib/api-key-vault-utils.ts create mode 100644 apps/desktop-ui/src/lib/secret-variables.ts create mode 100644 apps/desktop/src-tauri/src/http/mock_server.rs diff --git a/apps/desktop-ui/src/app/app/api-keys/page.tsx b/apps/desktop-ui/src/app/app/api-keys/page.tsx index b0c176f1..2e644108 100644 --- a/apps/desktop-ui/src/app/app/api-keys/page.tsx +++ b/apps/desktop-ui/src/app/app/api-keys/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef } from "react" import { AddApiKeyDialog } from "@/components/api-key-vault/add-api-key-dialog" import { ApiKeyList } from "@/components/api-key-vault/api-key-list" -import { useApiKeyVaultStore, type ApiKeyEntry, type ApiKeyEnv } from "@/store/api-key-vault-store" +import { useApiKeyVaultStore, type ApiKeyEntry } from "@/store/api-key-vault-store" import { ToolHeader } from "@/components/tools/tool-header" import { useVaultGuard } from "@/hooks/use-vault-guard" import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder" @@ -18,25 +18,7 @@ import { EncryptedToolPlaceholder } from "@/components/encrypted-tool-placeholde import { useActiveWorkspace } from "@/store/workspace-store" import { useCipherKey } from "@/lib/use-cipher-key" import { hasWorkspaceEncryption } from "@/lib/workspace-rbac" - -// ponytail: inline parser — one place uses it, no utils file -function parseApiKeyPayload(plain: string): Omit | null { - try { - const o = JSON.parse(plain) - if (typeof o !== "object" || o === null) return null - const env: ApiKeyEnv = - o.env === "staging" || o.env === "production" ? o.env : "development" - return { - name: typeof o.name === "string" ? o.name : "", - apiKey: typeof o.apiKey === "string" ? o.apiKey : "", - secret: typeof o.secret === "string" ? o.secret : "", - env, - notes: typeof o.notes === "string" ? o.notes : "", - } - } catch { - return null - } -} +import { parseApiKeyPayload } from "@/lib/api-key-vault-utils" export default function ApiKeyVaultPage() { // ALL hooks must be called before any early return (Rules of Hooks). diff --git a/apps/desktop-ui/src/components/api-client/api-client.tsx b/apps/desktop-ui/src/components/api-client/api-client.tsx index 6887139e..eb4d7b50 100644 --- a/apps/desktop-ui/src/components/api-client/api-client.tsx +++ b/apps/desktop-ui/src/components/api-client/api-client.tsx @@ -58,6 +58,8 @@ import { signDigest } from "@/lib/auth/digest" import { signJwt } from "@/lib/auth/jwt-bearer" import { cookieHeaderForUrl, storeCookiesFromResponse } from "@/lib/cookie-jar" import { resolveResponsePath } from "@/lib/response-path" +import { resolveSecretVariables } from "@/lib/secret-variables" +import { useCipherKey } from "@/lib/use-cipher-key" import { applyFolderInheritance, findRequestAncestors } from "@/lib/folder-inheritance" import { useJsonFormatter } from "./workers/use-json-formatter" import { useScriptsRunner } from "./workers/use-scripts-runner" @@ -120,6 +122,8 @@ function ApiClientInner() { const { addHistoryItem } = useHistoryActions() const { environments, activeEnvId, activeEnvironmentVariables } = useEnvironmentsState() const { setActiveEnvId, updateEnvironment } = useEnvironmentsActions() + // Cipher key for cross-tool {{vault.*}} / {{env.*}} secret resolution. + const cipherKey = useCipherKey() // Session-only variables — set by `pm.variables.set` in scripts and consumed by // the next request's variable substitution. Cleared on tab close. const sessionVarsRef = React.useRef>({}) @@ -365,6 +369,10 @@ function ApiClientInner() { // Previous response on the same tab — what `{{response.body.token}}` chains against. const previousResponse = activeTab.response + // Cross-tool secrets ({{vault.*}} / {{env.*}}) — resolved per send below, + // never cached in component state. + let secretVars: Record = {} + const substituteAll = (text: string): string => { if (!text) return text return text.replace(/\{\{(.+?)\}\}/g, (m, k) => { @@ -376,7 +384,7 @@ function ApiClientInner() { if (scriptEnvUnsets.has(key)) return m if (key in scriptEnvOverlay) return scriptEnvOverlay[key] if (key in sessionVarsRef.current) return sessionVarsRef.current[key] - return activeEnvironmentVariables[key] ?? m + return activeEnvironmentVariables[key] ?? secretVars[key] ?? m }) } @@ -423,6 +431,14 @@ function ApiClientInner() { workHeaders = r.request.headers } + // Resolve cross-tool secrets referenced anywhere in the request + // (scan the raw tab JSON so params/headers/auth/body are all covered). + // Throws a user-facing message when the vault is locked → outer catch. + secretVars = await resolveSecretVariables( + [workUrl, JSON.stringify(workHeaders), JSON.stringify(activeTab)], + cipherKey, + ) + // Substitute variables in URL const finalUrl = substituteAll(workUrl) @@ -782,10 +798,17 @@ function ApiClientInner() { }, }) - // Observability: record metric for the metrics dashboard. + // Observability: record metric for the metrics dashboard. Resolved + // {{vault.*}}/{{env.*}} secrets are redacted back to their tokens — + // the metrics panel renders these URLs. + const redactSecrets = (text: string): string => + Object.entries(secretVars).reduce( + (acc, [token, value]) => (value ? acc.split(value).join(`{{${token}}}`) : acc), + text, + ) recordMetric({ method: beforeApplied.req.method, - url: finalUrlFromPlugins, + url: redactSecrets(finalUrlFromPlugins), status: proxyData.status ?? 0, timeMs: proxyData.time ?? 0, sizeBytes: proxyData.size ?? 0, @@ -793,7 +816,7 @@ function ApiClientInner() { }) if (proxyData.error) { - recordLog({ level: "error", message: `${beforeApplied.req.method} ${finalUrlFromPlugins} → ${proxyData.error}` }) + recordLog({ level: "error", message: `${beforeApplied.req.method} ${redactSecrets(finalUrlFromPlugins)} → ${proxyData.error}` }) } // Cookie jar: persist Set-Cookie headers from the response. @@ -975,6 +998,7 @@ function ApiClientInner() { activeEnvId, environments, updateEnvironment, + cipherKey, t, ]) diff --git a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx index 633d5775..686de5f2 100644 --- a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx +++ b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx @@ -46,6 +46,7 @@ import { useCollectionsState, useCollectionsActions } from "../context/collectio import { useHistoryState, useHistoryActions } from "../context/history-context" import { CollectionsSidebarSkeleton, HistoryListSkeleton } from "../skeletons" import { useDebouncedValue } from "@/lib/use-debounced-value" +import { isDesktop } from "@/lib/desktop/is-desktop" interface CollectionsSidebarProps { onLoadRequest: (request: CollectionRequest) => void @@ -361,12 +362,21 @@ export function CollectionsSidebar({ { if (typeof window === "undefined") return - const baseUrl = `${window.location.origin}/api/mock/${collection.id}/` try { + let baseUrl: string + if (isDesktop()) { + // Desktop: real loopback HTTP server so curl / the + // user's own app can call the mock. + const { invoke } = await import("@tauri-apps/api/core") + const port = await invoke("mock_server_start") + baseUrl = `http://127.0.0.1:${port}/${collection.id}/` + } else { + baseUrl = `${window.location.origin}/api/mock/${collection.id}/` + } await navigator.clipboard.writeText(baseUrl) toast.success("Mock URL copied — append the request path to invoke") - } catch { - toast.error("Could not copy to clipboard") + } catch (e) { + toast.error((e as Error)?.message || "Could not copy to clipboard") } }} > @@ -388,7 +398,8 @@ export function CollectionsSidebar({ Copy share link - { if (typeof window === "undefined") return try { @@ -413,7 +424,7 @@ export function CollectionsSidebar({ > Publish as public mock - + } downloadCollectionAsPostman(collection)}> Export (Postman v2.1) diff --git a/apps/desktop-ui/src/components/api-client/import-dialog.tsx b/apps/desktop-ui/src/components/api-client/import-dialog.tsx index 2b912a4e..45fbe6a9 100644 --- a/apps/desktop-ui/src/components/api-client/import-dialog.tsx +++ b/apps/desktop-ui/src/components/api-client/import-dialog.tsx @@ -12,11 +12,12 @@ import { } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { detectImportFormat, type ImportFormat } from "@/lib/import/detect" -import { importPostmanCollection } from "@/lib/import/postman" +import { importPostmanCollectionWithMeta } from "@/lib/import/postman" import { importHar } from "@/lib/import/har" import { importOpenApiSpec } from "@/lib/import/openapi" import { generateMockExamplesFromOpenApi } from "@/lib/mocks/openapi-mock-gen" import { useCollectionsActions } from "./context/collections-context" +import { useEnvironmentsActions } from "./context/environments-context" import { toast } from "sonner" interface ImportDialogProps { @@ -34,6 +35,7 @@ const FORMAT_LABEL: Record = { export function ImportDialog({ open, onOpenChange }: ImportDialogProps) { const { importCollection } = useCollectionsActions() + const { addEnvironment, updateEnvironment } = useEnvironmentsActions() const [text, setText] = React.useState("") const [busy, setBusy] = React.useState(false) @@ -50,11 +52,15 @@ export function ImportDialog({ open, onOpenChange }: ImportDialogProps) { if (!canImport) return setBusy(true) try { - const collection = detected === "postman" - ? importPostmanCollection(text) - : detected === "har" - ? importHar(text) - : importOpenApiSpec(text) + let postmanVariables: Array<{ key: string; value: string }> = [] + let collection + if (detected === "postman") { + const result = importPostmanCollectionWithMeta(text) + collection = result.collection + postmanVariables = result.variables + } else { + collection = detected === "har" ? importHar(text) : importOpenApiSpec(text) + } // OpenAPI imports get auto-generated mock examples derived from the // spec's 2xx response schemas. Lets users hit the public mock route // immediately without crafting Save-as-Example entries by hand. @@ -63,6 +69,21 @@ export function ImportDialog({ open, onOpenChange }: ImportDialogProps) { } const created = await importCollection(collection) if (created) { + // Postman collection-level variables → an api-client environment, + // so {{baseUrl}}-style references resolve right after import. + if (postmanVariables.length) { + const envId = await addEnvironment(collection.name) + if (envId) { + await updateEnvironment(envId, { + variables: postmanVariables.map((v) => ({ + id: crypto.randomUUID(), + key: v.key, + value: v.value, + enabled: true, + })), + }) + } + } onOpenChange(false) setText("") } diff --git a/apps/desktop-ui/src/components/api-client/response-panel.tsx b/apps/desktop-ui/src/components/api-client/response-panel.tsx index 5e85dd1b..4d0a9ff5 100644 --- a/apps/desktop-ui/src/components/api-client/response-panel.tsx +++ b/apps/desktop-ui/src/components/api-client/response-panel.tsx @@ -302,6 +302,7 @@ export function ResponsePanel({ response, isLoading, scriptResults, onSaveExampl })()}
+
{isSuccess ? ( @@ -330,6 +331,8 @@ export function ResponsePanel({ response, isLoading, scriptResults, onSaveExampl {(response.size / 1024).toFixed(2)} KB
+
+
@@ -344,6 +347,7 @@ export function ResponsePanel({ response, isLoading, scriptResults, onSaveExampl +
{redirectChain.length > 0 && ( diff --git a/apps/desktop-ui/src/lib/__tests__/import-export.test.ts b/apps/desktop-ui/src/lib/__tests__/import-export.test.ts index cff258f3..ff7d1ee6 100644 --- a/apps/desktop-ui/src/lib/__tests__/import-export.test.ts +++ b/apps/desktop-ui/src/lib/__tests__/import-export.test.ts @@ -1,4 +1,4 @@ -import { importPostmanCollection } from "../import/postman" +import { importPostmanCollection, importPostmanCollectionWithMeta } from "../import/postman" import { importHar } from "../import/har" import { detectImportFormat } from "../import/detect" import { exportPostmanCollection } from "../export/postman" @@ -210,3 +210,126 @@ describe("Postman export round-trip", () => { expect(parsed.info.name).toBe("y") }) }) + +// ── Real-world Postman v2.1 shape: collection auth/scripts/variables, +// folder auth/scripts, graphql body ───────────────────────────────────────── +describe("importPostmanCollectionWithMeta (real-world v2.1)", () => { + const realWorld = JSON.stringify({ + info: { + name: "Acme API", + schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + }, + auth: { type: "bearer", bearer: [{ key: "token", value: "{{authToken}}", type: "string" }] }, + event: [ + { listen: "prerequest", script: { type: "text/javascript", exec: ["pm.environment.set('ts', Date.now())"] } }, + { listen: "test", script: { type: "text/javascript", exec: ["pm.test('status ok', () => pm.response.to.have.status(200))"] } }, + ], + variable: [ + { key: "baseUrl", value: "https://api.acme.test", type: "string" }, + { key: "authToken", value: "" }, + ], + item: [ + { + name: "Users", + auth: { type: "apikey", apikey: [{ key: "key", value: "X-Api-Key" }, { key: "value", value: "{{apiKey}}" }, { key: "in", value: "header" }] }, + event: [{ listen: "prerequest", script: { exec: "console.log('folder pre')" } }], + item: [ + { + name: "List users", + request: { + method: "GET", + url: { + raw: "{{baseUrl}}/users?page=1", + host: ["{{baseUrl}}"], + path: ["users"], + query: [{ key: "page", value: "1" }, { key: "debug", value: "true", disabled: true }], + }, + }, + }, + ], + }, + { + name: "Login", + request: { + method: "POST", + url: "{{baseUrl}}/login", + body: { mode: "raw", raw: '{"user":"a"}', options: { raw: { language: "json" } } }, + }, + event: [{ listen: "test", script: { exec: ["pm.test('has token', () => true)"] } }], + }, + { + name: "GraphQL query", + request: { + method: "POST", + url: "{{baseUrl}}/graphql", + body: { + mode: "graphql", + graphql: { query: "query { users { id } }", variables: '{"limit":10}' }, + }, + }, + }, + ], + }) + + it("surfaces collection variables", () => { + const { variables } = importPostmanCollectionWithMeta(realWorld) + expect(variables).toEqual([ + { key: "baseUrl", value: "https://api.acme.test" }, + { key: "authToken", value: "" }, + ]) + }) + + it("maps folder auth to defaultAuth and folder script to folder preRequestScript", () => { + const { collection } = importPostmanCollectionWithMeta(realWorld) + const folder = collection.items[0] as import("@/components/api-client/types").CollectionFolder + expect(folder.type).toBe("folder") + expect(folder.defaultAuth).toEqual({ + type: "api-key", + apiKeyKey: "X-Api-Key", + apiKeyValue: "{{apiKey}}", + apiKeyLocation: "header", + }) + expect(folder.preRequestScript).toContain("folder pre") + // Collection pre-script is prepended to the top-level folder. + expect(folder.preRequestScript!.indexOf("pm.environment.set")).toBeLessThan( + folder.preRequestScript!.indexOf("folder pre"), + ) + }) + + it("bakes collection auth into root requests without their own auth", () => { + const { collection } = importPostmanCollectionWithMeta(realWorld) + const login = collection.items[1] as CollectionRequest + expect(login.auth).toEqual({ type: "bearer", token: "{{authToken}}" }) + // Request under an auth-carrying folder inherits the folder auth at + // runtime instead — stays "none" here. + const folder = collection.items[0] as import("@/components/api-client/types").CollectionFolder + const listUsers = folder.items[0] as CollectionRequest + expect(listUsers.auth).toEqual({ type: "none" }) + }) + + it("prepends collection test script to root request's own tests", () => { + const { collection } = importPostmanCollectionWithMeta(realWorld) + const login = collection.items[1] as CollectionRequest + expect(login.testScript).toContain("status ok") + expect(login.testScript).toContain("has token") + expect(login.testScript!.indexOf("status ok")).toBeLessThan(login.testScript!.indexOf("has token")) + }) + + it("imports graphql bodies with query + variables", () => { + const { collection } = importPostmanCollectionWithMeta(realWorld) + const gql = collection.items[2] as CollectionRequest + expect(gql.body).toEqual({ + type: "graphql", + content: "query { users { id } }", + graphqlVariables: '{"limit":10}', + }) + }) + + it("keeps disabled query params inactive", () => { + const { collection } = importPostmanCollectionWithMeta(realWorld) + const folder = collection.items[0] as import("@/components/api-client/types").CollectionFolder + const listUsers = folder.items[0] as CollectionRequest + const debug = listUsers.params.find((p) => p.key === "debug") + expect(debug?.active).toBe(false) + }) +}) diff --git a/apps/desktop-ui/src/lib/__tests__/secret-variables.test.ts b/apps/desktop-ui/src/lib/__tests__/secret-variables.test.ts new file mode 100644 index 00000000..cf9ce9ec --- /dev/null +++ b/apps/desktop-ui/src/lib/__tests__/secret-variables.test.ts @@ -0,0 +1,58 @@ +import { collectSecretTokens, buildSecretMap } from "@/lib/secret-variables" + +const vaultKeys = [ + { name: "stripe", apiKey: "sk_live_123", secret: "whsec_456" }, + { name: "plain.secret", apiKey: "literal-wins", secret: "nope" }, +] + +const envSets = [ + { + project: "shop", + environment: "production", + variables: [ + { key: "DATABASE_URL", value: "postgres://prod" }, + { key: "API.BASE", value: "https://api.shop" }, + ], + }, + { project: "shop", environment: "staging", variables: [{ key: "DATABASE_URL", value: "postgres://staging" }] }, +] + +describe("collectSecretTokens", () => { + it("finds vault/env tokens, trims, dedupes, ignores plain vars", () => { + const tokens = collectSecretTokens([ + "https://x.test/{{ vault.stripe }}?a={{baseUrl}}", + '{"h":"{{env.shop.production.DATABASE_URL}}","again":"{{vault.stripe}}"}', + undefined, + ]) + expect(tokens.sort()).toEqual(["env.shop.production.DATABASE_URL", "vault.stripe"]) + }) +}) + +describe("buildSecretMap", () => { + it("resolves vault name to apiKey and .secret suffix to secret", () => { + const map = buildSecretMap(["vault.stripe", "vault.stripe.secret"], vaultKeys, []) + expect(map["vault.stripe"]).toBe("sk_live_123") + expect(map["vault.stripe.secret"]).toBe("whsec_456") + }) + + it("prefers an entry literally named with a .secret suffix", () => { + const map = buildSecretMap(["vault.plain.secret"], vaultKeys, []) + expect(map["vault.plain.secret"]).toBe("literal-wins") + }) + + it("resolves env..., keys may contain dots", () => { + const map = buildSecretMap( + ["env.shop.production.DATABASE_URL", "env.shop.staging.DATABASE_URL", "env.shop.production.API.BASE"], + [], + envSets, + ) + expect(map["env.shop.production.DATABASE_URL"]).toBe("postgres://prod") + expect(map["env.shop.staging.DATABASE_URL"]).toBe("postgres://staging") + expect(map["env.shop.production.API.BASE"]).toBe("https://api.shop") + }) + + it("omits unresolvable tokens", () => { + const map = buildSecretMap(["vault.missing", "env.shop.production.NOPE", "env.short"], vaultKeys, envSets) + expect(map).toEqual({}) + }) +}) diff --git a/apps/desktop-ui/src/lib/api-key-vault-utils.ts b/apps/desktop-ui/src/lib/api-key-vault-utils.ts new file mode 100644 index 00000000..154c7dbd --- /dev/null +++ b/apps/desktop-ui/src/lib/api-key-vault-utils.ts @@ -0,0 +1,20 @@ +import type { ApiKeyEntry, ApiKeyEnv } from "@/store/api-key-vault-store" + +/** Parse a decrypted API-key envelope payload; null on malformed JSON. */ +export function parseApiKeyPayload(plain: string): Omit | null { + try { + const o = JSON.parse(plain) + if (typeof o !== "object" || o === null) return null + const env: ApiKeyEnv = + o.env === "staging" || o.env === "production" ? o.env : "development" + return { + name: typeof o.name === "string" ? o.name : "", + apiKey: typeof o.apiKey === "string" ? o.apiKey : "", + secret: typeof o.secret === "string" ? o.secret : "", + env, + notes: typeof o.notes === "string" ? o.notes : "", + } + } catch { + return null + } +} diff --git a/apps/desktop-ui/src/lib/desktop/api-fetch.ts b/apps/desktop-ui/src/lib/desktop/api-fetch.ts index 3e12bff7..ab49a7d4 100644 --- a/apps/desktop-ui/src/lib/desktop/api-fetch.ts +++ b/apps/desktop-ui/src/lib/desktop/api-fetch.ts @@ -25,7 +25,9 @@ export async function apiFetch(path: string, init?: RequestInit): Promise } @@ -28,6 +29,7 @@ interface PostmanItem { item?: PostmanItem[] request?: PostmanRequest event?: PostmanEvent[] + auth?: PostmanAuth } interface PostmanEvent { @@ -70,6 +72,7 @@ interface PostmanBody { raw?: string urlencoded?: PostmanKV[] formdata?: PostmanKV[] + graphql?: { query?: string; variables?: string } options?: { raw?: { language?: string } } } @@ -140,7 +143,14 @@ function convertBody(b: PostmanBody | undefined): RequestBody { })) return { type: "form-data", content: "", formData: items } } - // graphql / file / unknown — degrade to text raw body. + if (b.mode === "graphql") { + return { + type: "graphql", + content: b.graphql?.query ?? "", + graphqlVariables: b.graphql?.variables || undefined, + } + } + // file / unknown — degrade to text raw body. return { type: "text", content: b.raw ?? "" } } @@ -193,7 +203,7 @@ function convertAuth(a: PostmanAuth | undefined): RequestAuth { return { type: "none" } } -function convertRequest(item: PostmanItem): CollectionRequest { +function convertRequest(item: PostmanItem, inheritedAuth?: RequestAuth): CollectionRequest { const r = item.request ?? {} const { url, query } = readUrl(r.url) const method = (r.method ?? "GET").toUpperCase() as RequestMethod @@ -209,25 +219,42 @@ function convertRequest(item: PostmanItem): CollectionRequest { params: kvList(query), headers: kvList(r.header), body: convertBody(r.body), - auth: convertAuth(r.auth), + // Postman semantics: no request-level auth → inherit from the nearest + // ancestor. Folder auth becomes folder defaultAuth (runtime merge); + // collection auth has no runtime home, so it's baked in here. + auth: r.auth ? convertAuth(r.auth) : inheritedAuth ?? { type: "none" }, preRequestScript: preRequestScript || undefined, testScript: testScript || undefined, } } -function convertItems(items: PostmanItem[] | undefined): Array { +function joinScripts(...parts: Array): string | undefined { + const cleaned = parts.map((p) => (p ?? "").trim()).filter(Boolean) + return cleaned.length ? cleaned.join("\n\n") : undefined +} + +function convertItems( + items: PostmanItem[] | undefined, + inheritedAuth?: RequestAuth, +): Array { if (!items) return [] const out: Array = [] for (const item of items) { if (item.request) { - out.push(convertRequest(item)) + out.push(convertRequest(item, inheritedAuth)) } else if (item.item) { + const folderAuth = item.auth ? convertAuth(item.auth) : undefined out.push({ id: id(), name: item.name ?? "Folder", type: "folder", - items: convertItems(item.item), + // Folder auth applies at runtime via defaultAuth, so children stop + // inheriting the collection-level fallback under such a folder. + items: convertItems(item.item, folderAuth ? undefined : inheritedAuth), isOpen: false, + defaultAuth: folderAuth, + preRequestScript: pickScript(item.event, "prerequest") || undefined, + testScript: pickScript(item.event, "test") || undefined, }) } // Items with neither `request` nor `item` are skipped silently — they're either @@ -236,15 +263,48 @@ function convertItems(items: PostmanItem[] | undefined): Array +} + +export function importPostmanCollectionWithMeta(raw: string | unknown): PostmanImportResult { const data: PostmanCollection = typeof raw === "string" ? JSON.parse(raw) : (raw as PostmanCollection) if (!data || typeof data !== "object") throw new Error("Not a Postman collection") if (!data.info?.name && !Array.isArray(data.item)) { throw new Error("Not a Postman collection (missing info.name + item[])") } + const collectionAuth = data.auth ? convertAuth(data.auth) : undefined + const items = convertItems(data.item, collectionAuth?.type === "none" ? undefined : collectionAuth) + + // Collection-level scripts run before everything else. Prepend them at the + // top level: folders cascade to children at runtime; bare root requests get + // them baked in. (Both CollectionFolder and CollectionRequest carry the + // same script fields.) + const pre = pickScript(data.event, "prerequest") + const test = pickScript(data.event, "test") + if (pre || test) { + for (const item of items) { + item.preRequestScript = joinScripts(pre, item.preRequestScript) + item.testScript = joinScripts(test, item.testScript) + } + } + + const variables = (data.variable ?? []) + .filter((v) => v && typeof v.key === "string" && v.key) + .map((v) => ({ key: v.key, value: v.value ?? "" })) + return { - id: id(), - name: data.info?.name ?? "Imported Postman collection", - items: convertItems(data.item), + collection: { + id: id(), + name: data.info?.name ?? "Imported Postman collection", + items, + }, + variables, } } + +export function importPostmanCollection(raw: string | unknown): Collection { + return importPostmanCollectionWithMeta(raw).collection +} diff --git a/apps/desktop-ui/src/lib/secret-variables.ts b/apps/desktop-ui/src/lib/secret-variables.ts new file mode 100644 index 00000000..6227d4e7 --- /dev/null +++ b/apps/desktop-ui/src/lib/secret-variables.ts @@ -0,0 +1,124 @@ +/** + * Cross-tool secret variables for the api-client. + * + * Request fields may reference secrets stored in the API Key Vault and the + * Environment Manager without copying them into api-client environments: + * + * {{vault.}} → API-key entry's `apiKey` (matched by entry name) + * {{vault..secret}} → that entry's `secret` field + * {{env...}} → Environment Manager variable + * + * Both stores are E2E-encrypted; resolution fetches and decrypts on demand + * with the active cipher key and returns a plain token→value map for the + * send-path substitution. Nothing is cached or persisted in plaintext. + * Project/environment names containing "." are not addressable (split on "."). + */ + +import { listApiKeyEntries } from "@/lib/api-key-vault-api" +import { listEnvSetEntries } from "@/lib/environment-manager-api" +import { decryptData } from "@/lib/encryption" +import { parseApiKeyPayload } from "@/lib/api-key-vault-utils" +import { parseEnvPayloadJson } from "@/lib/environment-manager-utils" + +export interface VaultKeyLike { + name: string + apiKey: string + secret: string +} + +export interface EnvSetLike { + project: string + environment: string + variables: Array<{ key: string; value: string }> +} + +/** Extract `vault.*` / `env.*` placeholder keys from raw request text. */ +export function collectSecretTokens(texts: Array): string[] { + const tokens = new Set() + for (const text of texts) { + if (!text) continue + for (const m of text.matchAll(/\{\{(.+?)\}\}/g)) { + const key = m[1].trim() + if (key.startsWith("vault.") || key.startsWith("env.")) tokens.add(key) + } + } + return [...tokens] +} + +/** Pure token→value resolution over already-decrypted entries. Unresolvable tokens are omitted. */ +export function buildSecretMap( + tokens: string[], + vaultKeys: VaultKeyLike[], + envSets: EnvSetLike[], +): Record { + const map: Record = {} + for (const token of tokens) { + if (token.startsWith("vault.")) { + const raw = token.slice("vault.".length) + // An entry literally named "" wins over the ".secret" reading. + const literal = vaultKeys.find((e) => e.name === raw) + if (literal) { + map[token] = literal.apiKey + continue + } + if (raw.endsWith(".secret")) { + const name = raw.slice(0, -".secret".length) + const entry = vaultKeys.find((e) => e.name === name) + if (entry) map[token] = entry.secret + } + } else { + const parts = token.slice("env.".length).split(".") + if (parts.length < 3) continue + const [project, environment, ...rest] = parts + const key = rest.join(".") + const set = envSets.find((s) => s.project === project && s.environment === environment) + const variable = set?.variables.find((v) => v.key === key) + if (variable) map[token] = variable.value + } + } + return map +} + +/** + * Resolve secret tokens found in `texts`. Returns {} when none are present. + * Throws a user-facing message when tokens exist but the vault is locked. + */ +export async function resolveSecretVariables( + texts: Array, + cipherKey: CryptoKey | null, +): Promise> { + const tokens = collectSecretTokens(texts) + if (tokens.length === 0) return {} + if (!cipherKey) { + throw new Error("Vault is locked — unlock your master password to resolve {{vault.*}} / {{env.*}} variables.") + } + const needVault = tokens.some((t) => t.startsWith("vault.")) + const needEnv = tokens.some((t) => t.startsWith("env.")) + const [keyRows, envRows] = await Promise.all([ + needVault ? listApiKeyEntries() : Promise.resolve([]), + needEnv ? listEnvSetEntries() : Promise.resolve([]), + ]) + const vaultKeys = ( + await Promise.all( + keyRows.map(async (row) => { + try { + return parseApiKeyPayload(await decryptData(cipherKey, row.encryptedData, row.iv)) + } catch { + return null + } + }), + ) + ).filter((e): e is NonNullable => e !== null) + const envSets = ( + await Promise.all( + envRows.map(async (row) => { + try { + return parseEnvPayloadJson(await decryptData(cipherKey, row.encryptedData, row.iv)) + } catch { + return null + } + }), + ) + ).filter((e): e is NonNullable => e !== null) + return buildSecretMap(tokens, vaultKeys, envSets) +} diff --git a/apps/desktop/src-tauri/src/http/mock_server.rs b/apps/desktop/src-tauri/src/http/mock_server.rs new file mode 100644 index 00000000..4ab7f6ad --- /dev/null +++ b/apps/desktop/src-tauri/src/http/mock_server.rs @@ -0,0 +1,286 @@ +//! Local mock server for the api-client. +//! +//! Serves a collection's saved response examples over a loopback HTTP +//! listener so external tools (curl, the user's own app) can call the mock: +//! +//! http://127.0.0.1:// +//! +//! Matching mirrors the web matcher (`lib/__tests__/mock-server.test.ts`): +//! HTTP method (case-insensitive) + exact pathname of the example's stored +//! URL; tree order, first match wins. The collection is re-read from the +//! local DB on every request, so edits to examples are live immediately. +//! Started lazily by the `mock_server_start` command; runs until app exit. + +use std::sync::{Mutex, OnceLock}; + +use base64::Engine; +use serde_json::Value; +use tauri::Manager; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +use crate::router::entries::{active_workspace, get_doc}; +use crate::state::AppState; + +/// Stable port so copied mock URLs survive app restarts; falls back to an +/// ephemeral port if something else holds it. +const PREFERRED_PORT: u16 = 8940; + +static RUNNING: OnceLock>> = OnceLock::new(); + +fn running() -> &'static Mutex> { + RUNNING.get_or_init(|| Mutex::new(None)) +} + +/// Start the mock server (idempotent) and return its port. +pub async fn start(app: tauri::AppHandle) -> Result { + if let Some(port) = *running().lock().unwrap() { + return Ok(port); + } + let listener = match TcpListener::bind(("127.0.0.1", PREFERRED_PORT)).await { + Ok(l) => l, + Err(_) => TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| format!("failed to bind mock server port: {e}"))?, + }; + let port = listener.local_addr().map_err(|e| e.to_string())?.port(); + *running().lock().unwrap() = Some(port); + + tauri::async_runtime::spawn(async move { + loop { + match listener.accept().await { + Ok((socket, _)) => { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let _ = handle_conn(socket, app).await; + }); + } + // Transient accept errors (e.g. fd exhaustion): back off, keep serving. + Err(_) => tokio::time::sleep(std::time::Duration::from_millis(100)).await, + } + } + }); + Ok(port) +} + +async fn handle_conn(mut socket: TcpStream, app: tauri::AppHandle) -> std::io::Result<()> { + // ponytail: single 64KB read — matching only needs the request line, which + // arrives in the first segment for any realistic client. Switch to + // read-until-blank-line if giant request lines ever truncate. + let mut buf = vec![0u8; 65536]; + let n = socket.read(&mut buf).await?; + let head = String::from_utf8_lossy(&buf[..n]); + let mut request_line = head.lines().next().unwrap_or("").split_whitespace(); + let method = request_line.next().unwrap_or("").to_string(); + let raw_path = request_line.next().unwrap_or("/"); + let path = raw_path.split('?').next().unwrap_or("/"); + + // CORS preflight so browser apps can call the mock. + if method.eq_ignore_ascii_case("OPTIONS") { + return write_response( + &mut socket, + 204, + "No Content", + "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: *\r\nAccess-Control-Allow-Headers: *\r\n", + b"", + ) + .await; + } + + let mut segments = path.trim_start_matches('/').splitn(2, '/'); + let collection_id = segments.next().unwrap_or("").to_string(); + let request_path = format!("/{}", segments.next().unwrap_or("")); + + // Scope the DB lock so the guard is dropped before any await. + let collection = { + let state = app.state::(); + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + get_doc(&db, "api_client_collections", &ws, &collection_id) + .ok() + .flatten() + }; + + let Some(collection) = collection else { + return write_json(&mut socket, 404, r#"{"error":"Collection not found"}"#).await; + }; + let items = collection["items"].as_array().cloned().unwrap_or_default(); + match find_first_match(&items, &method, &request_path) { + Some(response) => write_example(&mut socket, response).await, + None => { + let msg = format!( + r#"{{"error":"No matching example for {} {}"}}"#, + method.to_uppercase(), + request_path + ); + write_json(&mut socket, 404, &msg).await + } + } +} + +// ── Matcher (mirrors lib/__tests__/mock-server.test.ts) ──────────────────── + +fn find_first_match<'a>(items: &'a [Value], method: &str, pathname: &str) -> Option<&'a Value> { + for item in items { + if item["type"].as_str() == Some("folder") { + if let Some(children) = item["items"].as_array() { + if let Some(hit) = find_first_match(children, method, pathname) { + return Some(hit); + } + } + continue; + } + if let Some(examples) = item["examples"].as_array() { + for ex in examples { + if example_matches(ex, method, pathname) { + return Some(&ex["response"]); + } + } + } + } + None +} + +fn example_matches(ex: &Value, method: &str, pathname: &str) -> bool { + let ex_method = ex["request"]["method"].as_str().unwrap_or(""); + if !ex_method.eq_ignore_ascii_case(method) { + return false; + } + let url = ex["request"]["url"].as_str().unwrap_or(""); + // Web matcher: URL pathname, falling back to the literal string when + // unparseable (relative URLs), empty → "/". + let stored = match reqwest::Url::parse(url) { + Ok(u) => u.path().to_string(), + Err(_) if url.is_empty() => "/".to_string(), + Err(_) => url.to_string(), + }; + stored == pathname +} + +// ── Response writing ─────────────────────────────────────────────────────── + +async fn write_example(socket: &mut TcpStream, response: &Value) -> std::io::Result<()> { + let status = response["status"].as_u64().unwrap_or(200) as u16; + let status_text = response["statusText"].as_str().unwrap_or("").to_string(); + let body: Vec = if response["isBase64"].as_bool().unwrap_or(false) { + base64::engine::general_purpose::STANDARD + .decode(response["body"].as_str().unwrap_or("")) + .unwrap_or_default() + } else { + response["body"].as_str().unwrap_or("").as_bytes().to_vec() + }; + + let mut headers = String::new(); + let mut has_content_type = false; + if let Some(map) = response["headers"].as_object() { + for (k, v) in map { + let Some(value) = v.as_str() else { continue }; + // Framing headers describe the captured upstream transfer, not ours. + let key = k.to_lowercase(); + if ["content-length", "transfer-encoding", "connection", "content-encoding"] + .contains(&key.as_str()) + { + continue; + } + if k.contains(['\r', '\n']) || value.contains(['\r', '\n']) { + continue; + } + if key == "content-type" { + has_content_type = true; + } + headers.push_str(&format!("{k}: {value}\r\n")); + } + } + if !has_content_type { + if let Some(ct) = response["contentType"].as_str() { + if !ct.is_empty() && !ct.contains(['\r', '\n']) { + headers.push_str(&format!("Content-Type: {ct}\r\n")); + } + } + } + headers.push_str("Access-Control-Allow-Origin: *\r\n"); + write_response(socket, status, &status_text, &headers, &body).await +} + +async fn write_json(socket: &mut TcpStream, status: u16, body: &str) -> std::io::Result<()> { + write_response( + socket, + status, + "", + "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", + body.as_bytes(), + ) + .await +} + +async fn write_response( + socket: &mut TcpStream, + status: u16, + status_text: &str, + headers: &str, + body: &[u8], +) -> std::io::Result<()> { + let head = format!( + "HTTP/1.1 {status} {status_text}\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + socket.write_all(head.as_bytes()).await?; + socket.write_all(body).await?; + socket.flush().await +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn example(method: &str, url: &str) -> Value { + json!({ + "id": "x", "name": "ex", "capturedAt": 1, + "request": { "method": method, "url": url, "headers": [], "body": { "type": "none", "content": "" } }, + "response": { "status": 200, "statusText": "OK", "headers": {}, "body": "{}" } + }) + } + + fn request_with(examples: Vec) -> Value { + json!({ "id": "r", "name": "r", "method": "GET", "url": "https://api.test/", "examples": examples }) + } + + #[test] + fn matches_method_and_pathname_ignoring_host() { + let items = vec![request_with(vec![example("POST", "https://OTHER-HOST.test/v1/login")])]; + assert!(find_first_match(&items, "post", "/v1/login").is_some()); + assert!(find_first_match(&items, "GET", "/v1/login").is_none()); + assert!(find_first_match(&items, "POST", "/nope").is_none()); + } + + #[test] + fn walks_nested_folders() { + let inner = request_with(vec![example("GET", "https://api.test/deep")]); + let items = vec![json!({ "id": "f1", "name": "f", "type": "folder", "items": [inner] })]; + assert!(find_first_match(&items, "GET", "/deep").is_some()); + } + + #[test] + fn first_hit_wins() { + let mut a = example("GET", "https://api.test/dup"); + a["response"]["body"] = json!("a"); + let mut b = example("GET", "https://api.test/dup"); + b["response"]["body"] = json!("b"); + let items = vec![request_with(vec![a, b])]; + let hit = find_first_match(&items, "GET", "/dup").unwrap(); + assert_eq!(hit["body"], "a"); + } + + #[test] + fn unparseable_url_falls_back_to_literal() { + let items = vec![request_with(vec![example("GET", "/relative-url")])]; + assert!(find_first_match(&items, "GET", "/relative-url").is_some()); + } + + #[test] + fn bare_host_url_matches_root() { + let items = vec![request_with(vec![example("GET", "https://api.test")])]; + assert!(find_first_match(&items, "GET", "/").is_some()); + } +} diff --git a/apps/desktop/src-tauri/src/http/mod.rs b/apps/desktop/src-tauri/src/http/mod.rs index 8f96e6ed..7dc28812 100644 --- a/apps/desktop/src-tauri/src/http/mod.rs +++ b/apps/desktop/src-tauri/src/http/mod.rs @@ -1,3 +1,4 @@ pub mod auth_server; +pub mod mock_server; pub mod proxy; pub mod remote; diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index c2b87213..06ca7224 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -60,6 +60,11 @@ fn http_request_stream_cancel(id: u64) { http::proxy::cancel_stream(id); } +#[tauri::command] +async fn mock_server_start(app: tauri::AppHandle) -> Result { + http::mock_server::start(app).await +} + #[tauri::command] async fn await_browser_auth( port_channel: tauri::ipc::Channel, @@ -118,6 +123,7 @@ pub fn run() { http_request, http_request_stream, http_request_stream_cancel, + mock_server_start, await_browser_auth ]) .run(tauri::generate_context!()) From c30da1e32b0c0eb4562b96c2e423d3ad9ba4939f Mon Sep 17 00:00:00 2001 From: AKHIL Date: Mon, 20 Jul 2026 22:10:12 +0530 Subject: [PATCH 2/2] UI --- apps/desktop-ui/messages/af.json | 115 +++++++- apps/desktop-ui/messages/ar.json | 200 ++++++++++++- apps/desktop-ui/messages/ca.json | 200 ++++++++++++- apps/desktop-ui/messages/cs.json | 200 ++++++++++++- apps/desktop-ui/messages/da.json | 200 ++++++++++++- apps/desktop-ui/messages/de.json | 200 ++++++++++++- apps/desktop-ui/messages/el.json | 200 ++++++++++++- apps/desktop-ui/messages/en.json | 115 +++++++- apps/desktop-ui/messages/es.json | 115 +++++++- apps/desktop-ui/messages/fa.json | 115 +++++++- apps/desktop-ui/messages/fr.json | 115 +++++++- apps/desktop-ui/messages/id.json | 200 ++++++++++++- apps/desktop-ui/messages/it.json | 119 +++++++- apps/desktop-ui/messages/ja.json | 200 ++++++++++++- apps/desktop-ui/messages/ko.json | 200 ++++++++++++- apps/desktop-ui/messages/ms.json | 115 +++++++- apps/desktop-ui/messages/nb.json | 200 ++++++++++++- apps/desktop-ui/messages/nl.json | 200 ++++++++++++- apps/desktop-ui/messages/pl.json | 200 ++++++++++++- apps/desktop-ui/messages/pt-BR.json | 200 ++++++++++++- apps/desktop-ui/messages/pt.json | 200 ++++++++++++- apps/desktop-ui/messages/ru.json | 200 ++++++++++++- apps/desktop-ui/messages/sv.json | 115 +++++++- apps/desktop-ui/messages/tr.json | 115 +++++++- apps/desktop-ui/messages/uk.json | 115 +++++++- apps/desktop-ui/messages/vi.json | 200 ++++++++++++- apps/desktop-ui/messages/zh.json | 200 ++++++++++++- .../src/app/app/database-explorer/page.tsx | 91 +++--- .../src/app/app/sql-client/page.tsx | 15 +- .../src/app/app/to-do/TaskContainer.tsx | 2 + apps/desktop-ui/src/app/globals.css | 18 +- .../src/components/api-client/api-client.tsx | 18 +- .../collections/save-request-dialog.tsx | 71 ++++- .../api-client/collections/use-collections.ts | 2 + .../context/collections-context.tsx | 4 +- .../components/api-client/request-panel.tsx | 19 +- .../components/api-client/request-tabs.tsx | 2 +- .../components/api-client/response-panel.tsx | 2 +- .../bookmarks/bookmarks-manager.tsx | 2 + .../dashboard/dashboard-search-bar.tsx | 2 + .../email-validator/email-validator.tsx | 8 +- .../src/components/nosql-explorer/cells.tsx | 9 +- .../nosql-explorer/document-view.tsx | 229 +++++++++----- .../nosql-explorer/explorer-sidebar.tsx | 6 +- .../nosql-explorer/export-dialog.tsx | 1 + .../nosql-explorer/import-dialog.tsx | 28 +- .../nosql-explorer/index-manager.tsx | 194 ++++++++---- .../components/nosql-explorer/json-tree.tsx | 4 +- .../nosql-explorer/query-builder.tsx | 27 +- .../components/nosql-explorer/schema-view.tsx | 18 +- .../src/components/notes/NotesSidebar.tsx | 2 + .../password-manager/password-list.tsx | 2 + .../src/components/shell/top-nav-strip.tsx | 8 +- .../components/sql-client/query-editor.tsx | 166 ++++++++++- .../components/sql-client/results-table.tsx | 174 +++++++++-- .../src/components/sql-client/types.ts | 9 + .../src/components/tools/tool-page-header.tsx | 9 +- .../src/lib/__tests__/sql-cell.test.ts | 19 ++ apps/desktop-ui/src/lib/desktop/api-fetch.ts | 11 +- apps/desktop-ui/src/lib/nosql-pipeline.ts | 2 +- apps/desktop-ui/src/lib/sql-cell.ts | 9 + .../src/lib/user-preferences-api.ts | 34 +++ apps/desktop/src-tauri/Cargo.lock | 4 + apps/desktop/src-tauri/Cargo.toml | 6 +- apps/desktop/src-tauri/src/dbtools/mongo.rs | 81 +++-- apps/desktop/src-tauri/src/dbtools/sql.rs | 176 ++++++++++- apps/desktop/src-tauri/src/http/grpc.rs | 279 ++++++++++++++++++ apps/desktop/src-tauri/src/http/mod.rs | 1 + apps/desktop/src-tauri/src/http/proxy.rs | 2 +- apps/desktop/src-tauri/src/lib.rs | 6 + .../src-tauri/src/router/preferences.rs | 7 + 71 files changed, 5849 insertions(+), 484 deletions(-) create mode 100644 apps/desktop-ui/src/lib/__tests__/sql-cell.test.ts create mode 100644 apps/desktop-ui/src/lib/sql-cell.ts create mode 100644 apps/desktop/src-tauri/src/http/grpc.rs diff --git a/apps/desktop-ui/messages/af.json b/apps/desktop-ui/messages/af.json index 4275943f..4973c624 100644 --- a/apps/desktop-ui/messages/af.json +++ b/apps/desktop-ui/messages/af.json @@ -1580,7 +1580,10 @@ "cancel": "Kanselleer", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Kon nie # versameling uitvee nie} other {Kon nie # versamelings uitvee nie}}", + "bulkDeleted": "{count, plural, one {# versameling uitgevee} other {# versamelings uitgevee}}", + "bulkDeleteButton": "Vee {count, plural, one {# versameling} other {# versamelings}} uit" }, "document": { "docsBreadcrumb": "{n} dokumente", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} teruggegee", "explainDocsExamined": "{n} ondersoek", "explainCollscanHint": "Hierdie navraag deursoek elke dokument in die collection. Oorweeg 'n indeks op die gefiltreerde velde.", - "explainRawLabel": "Explain-uitset" + "explainRawLabel": "Explain-uitset", + "docInsertFailed": "Kon nie dokument invoeg nie", + "docUpdateFailed": "Kon nie dokument opdateer nie", + "indexesLoadFail": "Kon nie indekse laai nie", + "bulkDeleted": "{count, plural, one {# dokument uitgevee} other {# dokumente uitgevee}}", + "bulkDeleteFailed": "Massa-uitvee het misluk", + "selectedCount": "{count, plural, one {# dokument gekies} other {# dokumente gekies}}", + "deleteSelected": "Vee gekose uit", + "clearSelection": "Maak skoon", + "statusLoading": "Laai tans...", + "statusShowing": "Wys {from}–{to} van {total} dokumente", + "statusEmpty": "0 dokumente", + "statusSelected": "{count} gekies", + "bulkDeleteTitle": "Vee {count, plural, one {# dokument} other {# dokumente}} uit?", + "bulkDeleteDescription": "Dit sal {count, plural, one {# dokument} other {# dokumente}} permanent uit {collection} uitvee. Dit kan nie ongedaan gemaak word nie.", + "bulkDeleting": "Vee tans uit...", + "bulkDeleteConfirm": "Vee alles uit", + "queryErrorTitle": "Kon nie dokumente laai nie", + "retry": "Probeer weer" }, "tabs": { "filterActive": "Aktiewe filter toegepas", @@ -1733,7 +1754,8 @@ "cancel": "Kanselleer", "export": "Uitvoer", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Voer slegs die huidige bladsy uit ({count, plural, one {# dokument} other {# dokumente}})" }, "jsonTree": { "typeLabel": "Tipe: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Voorskouresultate ({count})", "previewEmpty": "Geen dokumente in hierdie fase nie", "previewFail": "Voorskou het misluk" + }, + "indexManager": { + "loading": "Laai tans indekse...", + "retry": "Probeer weer", + "countLabel": "{count, plural, one {# indeks} other {# indekse}}", + "totalSize": "{size} totaal", + "statsDocs": "{count, plural, one {# dok.} other {# dok.}}", + "statsStorage": "{size} berging", + "statsAvgObj": "{size} gem./dok.", + "newIndex": "Nuwe indeks", + "createTitle": "Skep indeks", + "fieldPlaceholder": "Veldnaam", + "ascending": "Stygend", + "descending": "Dalend", + "addField": "Voeg veld by", + "unique": "Uniek", + "sparse": "Sparse", + "ttlLabel": "TTL (sekondes)", + "ttlPlaceholder": "bv. 3600", + "cancel": "Kanselleer", + "create": "Skep", + "creating": "Skep tans...", + "created": "Indeks geskep", + "createFailed": "Kon nie indeks skep nie", + "fieldRequired": "Veldnaam word vereis", + "dropped": "Indeks \"{name}\" is laat val", + "dropFailed": "Kon nie indeks laat val nie", + "dropTitle": "Laat indeks val?", + "dropDescription": "Dit sal indeks \"{name}\" permanent laat val. Navrae wat hierdie indeks gebruik, sal stadiger word.", + "dropping": "Laat tans val...", + "dropConfirm": "Laat indeks val", + "badgeSystem": "stelsel", + "badgeUnique": "uniek", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Geen indekse gevind nie" + }, + "importDialog": { + "title": "Voer dokumente in {collection} in", + "onlyJson": "Slegs .json-lêers word ondersteun", + "invalidStructure": "Lêer moet 'n JSON-array of -objek bevat", + "invalidJson": "Ongeldige JSON: kon nie lêer ontleed nie", + "imported": "{count, plural, one {# dokument ingevoer} other {# dokumente ingevoer}}", + "importFailed": "Invoer het misluk", + "dropHint": "Sleep en los of klik om op te laai", + "dropSubHint": "Ondersteun JSON-array of NDJSON", + "docsCount": "{count, plural, one {# dok.} other {# dok.}}", + "previewLabel": "Voorskou (eerste 3 dokumente)", + "moreDocs": "... en nog {count} dokumente", + "cancel": "Kanselleer", + "importing": "Voer tans in...", + "importCount": "{count, plural, one {Voer # dok. in} other {Voer # dok. in}}", + "importBtn": "Voer in" + }, + "schemaView": { + "analyzing": "Ontleed tans skema...", + "loadFailed": "Kon nie skema ontleed nie", + "retry": "Probeer weer", + "noDocs": "Geen dokumente om te ontleed nie", + "sampled": "{docs} dokumente gemonster · {fields} velde", + "colField": "Veld", + "colTypes": "Tipes", + "colCoverage": "Dekking" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL geplak en suksesvol ontleed", "responseCopied": "Antwoord na knipbord gekopieer", "codeCopied": "Kode na knipbord gekopieer", - "copyFailed": "Failed to copy to clipboard" + "copyFailed": "Failed to copy to clipboard", + "curlCopied": "cURL-opdrag gekopieer" }, "layout": { "collections": "Versamelings", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Besig om te stuur...", "send": "Stuur", - "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid" + "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid", + "copyCurl": "Kopieer as cURL" }, "requestTabs": { "params": "Params", @@ -1915,7 +2002,10 @@ "placeholderName": "My versoek", "labelFolder": "Vouer", "placeholderFolder": "Kies 'n vouer", - "save": "Stoor" + "save": "Stoor", + "newFolder": "Nuwe gids", + "newFolderPlaceholder": "Gidsnaam", + "create": "Skep" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Navraag word uitgevoer…", "emptyTitle": "Voer 'n navraag uit om resultate te sien", "emptyHint": "Druk ⌘↩ of klik Voer uit", - "toastNoConnection": "Geen aktiewe verbinding nie." + "toastNoConnection": "Geen aktiewe verbinding nie.", + "btnHistory": "Geskiedenis", + "btnSaveQuery": "Stoor navraag", + "savedSection": "Gestoor", + "recentSection": "Onlangs", + "emptyHistory": "Nog niks hier nie — voer 'n navraag uit of stoor een", + "savePlaceholder": "Navraagnaam", + "deleteSaved": "Verwyder", + "toastQuerySaved": "Navraag gestoor" }, "results": { "filterPlaceholder": "Filtreer resultate…", + "editHint": "Dubbelklik op 'n sel om te wysig", + "toastRowUpdated": "Ry opgedateer", + "toastRowsUpdated": "{count, plural, one {# ry opgedateer} other {# rye opgedateer}}", "rowCount": "{count} ry(e)", "rowCountFiltered": "{filtered} / {total} rye", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ar.json b/apps/desktop-ui/messages/ar.json index 7d1c7e06..0d61ee09 100644 --- a/apps/desktop-ui/messages/ar.json +++ b/apps/desktop-ui/messages/ar.json @@ -1580,7 +1580,13 @@ "cancel": "إلغاء", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {فشل حذف # مجموعة} other {فشل حذف # مجموعات}}", + "bulkDeleted": "{count, plural, one {تم حذف # مجموعة} other {تم حذف # مجموعات}}", + "bulkDeleteButton": "{count, plural, one {حذف # مجموعة} other {حذف # مجموعات}}", + "bulkDeleteFailed": "{count, plural, one {فشل حذف مجموعة واحدة} other {فشل حذف # مجموعة}}", + "bulkDeleted": "{count, plural, one {تم حذف مجموعة واحدة} other {تم حذف # مجموعة}}", + "bulkDeleteButton": "حذف {count, plural, one {مجموعة واحدة} other {# مجموعة}}" }, "document": { "docsBreadcrumb": "{n} مستندات", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} مُرجع", "explainDocsExamined": "{n} مفحوص", "explainCollscanHint": "يفحص هذا الاستعلام كل مستند في المجموعة. فكر في إنشاء فهرس على الحقول المصفاة.", - "explainRawLabel": "مخرجات explain" + "explainRawLabel": "مخرجات explain", + "docInsertFailed": "فشل إدراج المستند", + "docUpdateFailed": "فشل تحديث المستند", + "indexesLoadFail": "فشل تحميل الفهارس", + "bulkDeleted": "{count, plural, one {تم حذف # مستند} other {تم حذف # مستندات}}", + "bulkDeleteFailed": "فشل الحذف الجماعي", + "selectedCount": "{count, plural, one {تم تحديد # مستند} other {تم تحديد # مستندات}}", + "deleteSelected": "حذف المحدد", + "clearSelection": "مسح", + "statusLoading": "جارٍ التحميل…", + "statusShowing": "عرض {from}–{to} من {total} مستند", + "statusEmpty": "0 مستند", + "statusSelected": "{count} محدد", + "bulkDeleteTitle": "حذف {count, plural, one {# مستند} other {# مستندات}}؟", + "bulkDeleteDescription": "سيؤدي هذا إلى حذف {count, plural, one {# مستند} other {# مستندات}} نهائيًا من {collection}. لا يمكن التراجع عن هذا الإجراء.", + "bulkDeleting": "جارٍ الحذف…", + "bulkDeleteConfirm": "حذف الكل", + "queryErrorTitle": "فشل تحميل المستندات", + "retry": "إعادة المحاولة", + "docInsertFailed": "فشل إدراج المستند", + "docUpdateFailed": "فشل تحديث المستند", + "indexesLoadFail": "فشل تحميل الفهارس", + "bulkDeleted": "{count, plural, one {تم حذف مستند واحد} other {تم حذف # مستند}}", + "bulkDeleteFailed": "فشل الحذف الجماعي", + "selectedCount": "{count, plural, one {تم تحديد مستند واحد} other {تم تحديد # مستند}}", + "deleteSelected": "حذف المحدد", + "clearSelection": "مسح", + "statusLoading": "جارٍ التحميل...", + "statusShowing": "عرض {from}-{to} من {total} مستند", + "statusEmpty": "0 مستند", + "statusSelected": "تم تحديد {count}", + "bulkDeleteTitle": "حذف {count, plural, one {مستند واحد} other {# مستند}}؟", + "bulkDeleteDescription": "سيؤدي هذا إلى حذف {count, plural, one {مستند واحد} other {# مستند}} نهائيًا من {collection}. لا يمكن التراجع عن هذا الإجراء.", + "bulkDeleting": "جارٍ الحذف...", + "bulkDeleteConfirm": "حذف الكل", + "queryErrorTitle": "فشل تحميل المستندات", + "retry": "إعادة المحاولة" }, "tabs": { "filterActive": "مرشح نشط مطبّق", @@ -1733,7 +1775,9 @@ "cancel": "إلغاء", "export": "تصدير", "sheetName": "بيانات", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "يصدّر الصفحة الحالية فقط ({count, plural, one {# مستند} other {# مستندات}})", + "pageOnlyNote": "يصدّر الصفحة الحالية فقط ({count, plural, one {مستند واحد} other {# مستند}})" }, "jsonTree": { "typeLabel": "النوع: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "نتائج المعاينة ({count})", "previewEmpty": "لا توجد مستندات في هذه المرحلة", "previewFail": "فشلت المعاينة" + }, + "indexManager": { + "loading": "جارٍ تحميل الفهارس…", + "retry": "إعادة المحاولة", + "countLabel": "{count, plural, one {# فهرس} other {# فهارس}}", + "totalSize": "{size} إجمالاً", + "statsDocs": "{count, plural, one {# مستند} other {# مستند}}", + "statsStorage": "{size} تخزين", + "statsAvgObj": "{size} متوسط/مستند", + "newIndex": "فهرس جديد", + "createTitle": "إنشاء فهرس", + "fieldPlaceholder": "اسم الحقل", + "ascending": "تصاعدي", + "descending": "تنازلي", + "addField": "إضافة حقل", + "unique": "فريد", + "sparse": "متفرق", + "ttlLabel": "TTL (ثوانٍ)", + "ttlPlaceholder": "مثال: 3600", + "cancel": "إلغاء", + "create": "إنشاء", + "creating": "جارٍ الإنشاء…", + "created": "تم إنشاء الفهرس", + "createFailed": "فشل إنشاء الفهرس", + "fieldRequired": "اسم الحقل مطلوب", + "dropped": "تم حذف الفهرس \"{name}\"", + "dropFailed": "فشل حذف الفهرس", + "dropTitle": "حذف الفهرس؟", + "dropDescription": "سيؤدي هذا إلى حذف الفهرس \"{name}\" نهائيًا. ستصبح الاستعلامات التي تستخدم هذا الفهرس أبطأ.", + "dropping": "جارٍ الحذف…", + "dropConfirm": "حذف الفهرس", + "badgeSystem": "نظام", + "badgeUnique": "فريد", + "badgeSparse": "متفرق", + "badgeTtl": "TTL", + "empty": "لم يتم العثور على فهارس" + }, + "importDialog": { + "title": "استيراد مستندات إلى {collection}", + "onlyJson": "يتم دعم ملفات .json فقط", + "invalidStructure": "يجب أن يحتوي الملف على مصفوفة أو كائن JSON", + "invalidJson": "JSON غير صالح: تعذّر تحليل الملف", + "imported": "{count, plural, one {تم استيراد # مستند} other {تم استيراد # مستندات}}", + "importFailed": "فشل الاستيراد", + "dropHint": "اسحب وأفلت أو انقر للتحميل", + "dropSubHint": "يدعم مصفوفة JSON أو NDJSON", + "docsCount": "{count, plural, one {# مستند} other {# مستندات}}", + "previewLabel": "معاينة (أول 3 مستندات)", + "moreDocs": "… و{count} مستندات أخرى", + "cancel": "إلغاء", + "importing": "جارٍ الاستيراد…", + "importCount": "{count, plural, one {استيراد # مستند} other {استيراد # مستندات}}", + "importBtn": "استيراد" + }, + "schemaView": { + "analyzing": "جارٍ تحليل المخطط…", + "loadFailed": "فشل تحليل المخطط", + "retry": "إعادة المحاولة", + "noDocs": "لا توجد مستندات للتحليل", + "sampled": "تم أخذ عينة من {docs} مستند · {fields} حقل", + "colField": "الحقل", + "colTypes": "الأنواع", + "colCoverage": "التغطية" + }, + "indexManager": { + "loading": "جارٍ تحميل الفهارس...", + "retry": "إعادة المحاولة", + "countLabel": "{count, plural, one {فهرس واحد} other {# فهرس}}", + "totalSize": "{size} إجمالاً", + "statsDocs": "{count, plural, one {مستند واحد} other {# مستند}}", + "statsStorage": "{size} تخزين", + "statsAvgObj": "{size} متوسط/مستند", + "newIndex": "فهرس جديد", + "createTitle": "إنشاء فهرس", + "fieldPlaceholder": "اسم الحقل", + "ascending": "تصاعدي", + "descending": "تنازلي", + "addField": "إضافة حقل", + "unique": "فريد", + "sparse": "متفرق", + "ttlLabel": "TTL (بالثواني)", + "ttlPlaceholder": "مثال: 3600", + "cancel": "إلغاء", + "create": "إنشاء", + "creating": "جارٍ الإنشاء...", + "created": "تم إنشاء الفهرس", + "createFailed": "فشل إنشاء الفهرس", + "fieldRequired": "اسم الحقل مطلوب", + "dropped": "تم إسقاط الفهرس \"{name}\"", + "dropFailed": "فشل إسقاط الفهرس", + "dropTitle": "إسقاط الفهرس؟", + "dropDescription": "سيؤدي هذا إلى إسقاط الفهرس \"{name}\" نهائيًا. ستصبح الاستعلامات التي تستخدم هذا الفهرس أبطأ.", + "dropping": "جارٍ الإسقاط...", + "dropConfirm": "إسقاط الفهرس", + "badgeSystem": "نظام", + "badgeUnique": "فريد", + "badgeSparse": "متفرق", + "badgeTtl": "TTL", + "empty": "لم يتم العثور على فهارس" + }, + "importDialog": { + "title": "استيراد المستندات إلى {collection}", + "onlyJson": "ملفات .json فقط مدعومة", + "invalidStructure": "يجب أن يحتوي الملف على مصفوفة أو كائن JSON", + "invalidJson": "JSON غير صالح: تعذّر تحليل الملف", + "imported": "{count, plural, one {تم استيراد مستند واحد} other {تم استيراد # مستند}}", + "importFailed": "فشل الاستيراد", + "dropHint": "اسحب وأفلت أو انقر للرفع", + "dropSubHint": "يدعم مصفوفة JSON أو NDJSON", + "docsCount": "{count, plural, one {مستند واحد} other {# مستند}}", + "previewLabel": "معاينة (أول 3 مستندات)", + "moreDocs": "... و{count} مستند آخر", + "cancel": "إلغاء", + "importing": "جارٍ الاستيراد...", + "importCount": "{count, plural, one {استيراد مستند واحد} other {استيراد # مستند}}", + "importBtn": "استيراد" + }, + "schemaView": { + "analyzing": "جارٍ تحليل المخطط...", + "loadFailed": "فشل تحليل المخطط", + "retry": "إعادة المحاولة", + "noDocs": "لا توجد مستندات للتحليل", + "sampled": "تم أخذ عينة من {docs} مستند · {fields} حقل", + "colField": "الحقل", + "colTypes": "الأنواع", + "colCoverage": "التغطية" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "تم لصق cURL وتحليله بنجاح", "responseCopied": "تم نسخ الاستجابة", "codeCopied": "تم نسخ الكود", - "copyFailed": "فشل نسخ المحتوى إلى الحافظة" + "copyFailed": "فشل نسخ المحتوى إلى الحافظة", + "curlCopied": "تم نسخ أمر cURL" }, "layout": { "collections": "المجموعات", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "جاري الإرسال...", "send": "إرسال", - "invalidJsonBodyHelp": "لا يمكن الإرسال: نص JSON غير صالح" + "invalidJsonBodyHelp": "لا يمكن الإرسال: نص JSON غير صالح", + "copyCurl": "نسخ كأمر cURL" }, "requestTabs": { "params": "معاملات", @@ -1915,7 +2087,10 @@ "placeholderName": "طلبي", "labelFolder": "المجلد", "placeholderFolder": "اختر مجلداً", - "save": "حفظ" + "save": "حفظ", + "newFolder": "مجلد جديد", + "newFolderPlaceholder": "اسم المجلد", + "create": "إنشاء" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "جارٍ تنفيذ الاستعلام…", "emptyTitle": "شغّل استعلاماً لرؤية النتائج", "emptyHint": "اضغط ⌘↩ أو انقر على تشغيل", - "toastNoConnection": "لا يوجد اتصال نشط." + "toastNoConnection": "لا يوجد اتصال نشط.", + "btnHistory": "السجل", + "btnSaveQuery": "حفظ الاستعلام", + "savedSection": "المحفوظة", + "recentSection": "الأخيرة", + "emptyHistory": "لا يوجد شيء هنا بعد — نفّذ استعلامًا أو احفظه", + "savePlaceholder": "اسم الاستعلام", + "deleteSaved": "حذف", + "toastQuerySaved": "تم حفظ الاستعلام" }, "results": { "filterPlaceholder": "تصفية النتائج…", + "editHint": "انقر نقرًا مزدوجًا على خلية للتحرير", + "toastRowUpdated": "تم تحديث الصف", + "toastRowsUpdated": "{count, plural, zero {لم يتم تحديث أي صف} one {تم تحديث صف واحد} two {تم تحديث صفين} few {تم تحديث # صفوف} many {تم تحديث # صفًا} other {تم تحديث # صف}}", "rowCount": "{count} صف", "rowCountFiltered": "{filtered} / {total} صف", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ca.json b/apps/desktop-ui/messages/ca.json index 5dfbc260..8db95bdf 100644 --- a/apps/desktop-ui/messages/ca.json +++ b/apps/desktop-ui/messages/ca.json @@ -1580,7 +1580,13 @@ "cancel": "Cancel·la", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {No s'ha pogut eliminar # col·lecció} other {No s'han pogut eliminar # col·leccions}}", + "bulkDeleted": "{count, plural, one {# col·lecció eliminada} other {# col·leccions eliminades}}", + "bulkDeleteButton": "{count, plural, one {Elimina # col·lecció} other {Elimina # col·leccions}}", + "bulkDeleteFailed": "{count, plural, one {No s'ha pogut suprimir # col·lecció} other {No s'han pogut suprimir # col·leccions}}", + "bulkDeleted": "{count, plural, one {S'ha suprimit # col·lecció} other {S'han suprimit # col·leccions}}", + "bulkDeleteButton": "Suprimeix {count, plural, one {# col·lecció} other {# col·leccions}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} retornats", "explainDocsExamined": "{n} examinats", "explainCollscanHint": "Aquesta consulta escaneja tots els documents de la col·lecció. Considera crear un índex sobre els camps filtrats.", - "explainRawLabel": "Sortida d'explain" + "explainRawLabel": "Sortida d'explain", + "docInsertFailed": "No s'ha pogut inserir el document", + "docUpdateFailed": "No s'ha pogut actualitzar el document", + "indexesLoadFail": "No s'han pogut carregar els índexs", + "bulkDeleted": "{count, plural, one {# document eliminat} other {# documents eliminats}}", + "bulkDeleteFailed": "Ha fallat l'eliminació massiva", + "selectedCount": "{count, plural, one {# document seleccionat} other {# documents seleccionats}}", + "deleteSelected": "Elimina els seleccionats", + "clearSelection": "Neteja", + "statusLoading": "S'està carregant…", + "statusShowing": "{from}–{to} de {total} documents", + "statusEmpty": "0 documents", + "statusSelected": "{count} seleccionats", + "bulkDeleteTitle": "Voleu eliminar {count, plural, one {# document} other {# documents}}?", + "bulkDeleteDescription": "Això eliminarà permanentment {count, plural, one {# document} other {# documents}} de {collection}. No es pot desfer.", + "bulkDeleting": "S'està eliminant…", + "bulkDeleteConfirm": "Elimina-ho tot", + "queryErrorTitle": "No s'han pogut carregar els documents", + "retry": "Torna-ho a provar", + "docInsertFailed": "No s'ha pogut inserir el document", + "docUpdateFailed": "No s'ha pogut actualitzar el document", + "indexesLoadFail": "No s'han pogut carregar els índexs", + "bulkDeleted": "{count, plural, one {# document suprimit} other {# documents suprimits}}", + "bulkDeleteFailed": "Ha fallat la supressió massiva", + "selectedCount": "{count, plural, one {# document seleccionat} other {# documents seleccionats}}", + "deleteSelected": "Suprimeix la selecció", + "clearSelection": "Neteja", + "statusLoading": "S'està carregant…", + "statusShowing": "Mostrant {from}–{to} de {total} documents", + "statusEmpty": "0 documents", + "statusSelected": "{count} seleccionats", + "bulkDeleteTitle": "Voleu suprimir {count, plural, one {# document} other {# documents}}?", + "bulkDeleteDescription": "Això suprimirà permanentment {count, plural, one {# document} other {# documents}} de {collection}. Aquesta acció no es pot desfer.", + "bulkDeleting": "S'està suprimint…", + "bulkDeleteConfirm": "Suprimeix-ho tot", + "queryErrorTitle": "No s'han pogut carregar els documents", + "retry": "Torna-ho a provar" }, "tabs": { "filterActive": "Filtre actiu aplicat", @@ -1733,7 +1775,9 @@ "cancel": "Cancel·la", "export": "Exporta", "sheetName": "Dades", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporta només la pàgina actual ({count, plural, one {# document} other {# documents}})", + "pageOnlyNote": "Només exporta la pàgina actual ({count, plural, one {# document} other {# documents}})" }, "jsonTree": { "typeLabel": "Tipus: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Resultats de previsualització ({count})", "previewEmpty": "Cap document en aquesta etapa", "previewFail": "La previsualització ha fallat" + }, + "indexManager": { + "loading": "S'estan carregant els índexs…", + "retry": "Torna-ho a provar", + "countLabel": "{count, plural, one {# índex} other {# índexs}}", + "totalSize": "{size} en total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} d'emmagatzematge", + "statsAvgObj": "{size} mitj./doc", + "newIndex": "Índex nou", + "createTitle": "Crea un índex", + "fieldPlaceholder": "Nom del camp", + "ascending": "Ascendent", + "descending": "Descendent", + "addField": "Afegeix un camp", + "unique": "Únic", + "sparse": "Sparse", + "ttlLabel": "TTL (segons)", + "ttlPlaceholder": "p. ex. 3600", + "cancel": "Cancel·la", + "create": "Crea", + "creating": "S'està creant…", + "created": "Índex creat", + "createFailed": "No s'ha pogut crear l'índex", + "fieldRequired": "El nom del camp és obligatori", + "dropped": "Índex \"{name}\" eliminat", + "dropFailed": "No s'ha pogut eliminar l'índex", + "dropTitle": "Voleu eliminar l'índex?", + "dropDescription": "Això eliminarà permanentment l'índex \"{name}\". Les consultes que l'utilitzen seran més lentes.", + "dropping": "S'està eliminant…", + "dropConfirm": "Elimina l'índex", + "badgeSystem": "sistema", + "badgeUnique": "únic", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "No s'ha trobat cap índex" + }, + "importDialog": { + "title": "Importa documents a {collection}", + "onlyJson": "Només s'admeten fitxers .json", + "invalidStructure": "El fitxer ha de contenir una matriu o un objecte JSON", + "invalidJson": "JSON no vàlid: no s'ha pogut analitzar el fitxer", + "imported": "{count, plural, one {# document importat} other {# documents importats}}", + "importFailed": "Ha fallat la importació", + "dropHint": "Arrossega i deixa anar o fes clic per pujar", + "dropSubHint": "Admet matriu JSON o NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Vista prèvia (primers 3 documents)", + "moreDocs": "… i {count} documents més", + "cancel": "Cancel·la", + "importing": "S'està important…", + "importCount": "{count, plural, one {Importa # doc} other {Importa # docs}}", + "importBtn": "Importa" + }, + "schemaView": { + "analyzing": "S'està analitzant l'esquema…", + "loadFailed": "No s'ha pogut analitzar l'esquema", + "retry": "Torna-ho a provar", + "noDocs": "No hi ha documents per analitzar", + "sampled": "{docs} documents mostrejats · {fields} camps", + "colField": "Camp", + "colTypes": "Tipus", + "colCoverage": "Cobertura" + }, + "indexManager": { + "loading": "S'estan carregant els índexs…", + "retry": "Torna-ho a provar", + "countLabel": "{count, plural, one {# índex} other {# índexs}}", + "totalSize": "{size} en total", + "statsDocs": "{count, plural, one {# doc.} other {# docs.}}", + "statsStorage": "{size} d'emmagatzematge", + "statsAvgObj": "{size} mitjana/doc.", + "newIndex": "Índex nou", + "createTitle": "Crea un índex", + "fieldPlaceholder": "Nom del camp", + "ascending": "Ascendent", + "descending": "Descendent", + "addField": "Afegeix un camp", + "unique": "Únic", + "sparse": "Dispers", + "ttlLabel": "TTL (segons)", + "ttlPlaceholder": "p. ex. 3600", + "cancel": "Cancel·la", + "create": "Crea", + "creating": "S'està creant…", + "created": "Índex creat", + "createFailed": "No s'ha pogut crear l'índex", + "fieldRequired": "El nom del camp és obligatori", + "dropped": "S'ha eliminat l'índex \"{name}\"", + "dropFailed": "No s'ha pogut eliminar l'índex", + "dropTitle": "Voleu eliminar l'índex?", + "dropDescription": "Això eliminarà permanentment l'índex \"{name}\". Les consultes que l'utilitzin seran més lentes.", + "dropping": "S'està eliminant…", + "dropConfirm": "Elimina l'índex", + "badgeSystem": "sistema", + "badgeUnique": "únic", + "badgeSparse": "dispers", + "badgeTtl": "TTL", + "empty": "No s'ha trobat cap índex" + }, + "importDialog": { + "title": "Importa documents a {collection}", + "onlyJson": "Només s'admeten fitxers .json", + "invalidStructure": "El fitxer ha de contenir un array o un objecte JSON", + "invalidJson": "JSON no vàlid: no s'ha pogut analitzar el fitxer", + "imported": "{count, plural, one {S'ha importat # document} other {S'han importat # documents}}", + "importFailed": "Ha fallat la importació", + "dropHint": "Arrossega i deixa anar o fes clic per pujar", + "dropSubHint": "Admet array JSON o NDJSON", + "docsCount": "{count, plural, one {# doc.} other {# docs.}}", + "previewLabel": "Vista prèvia (primers 3 documents)", + "moreDocs": "… i {count} documents més", + "cancel": "Cancel·la", + "importing": "S'està important…", + "importCount": "{count, plural, one {Importa # doc.} other {Importa # docs.}}", + "importBtn": "Importa" + }, + "schemaView": { + "analyzing": "S'està analitzant l'esquema…", + "loadFailed": "No s'ha pogut analitzar l'esquema", + "retry": "Torna-ho a provar", + "noDocs": "No hi ha documents per analitzar", + "sampled": "S'han mostrejat {docs} documents · {fields} camps", + "colField": "Camp", + "colTypes": "Tipus", + "colCoverage": "Cobertura" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL enganxat i analitzat correctament", "responseCopied": "Resposta copiada al porta-retalls", "codeCopied": "Codi copiat al porta-retalls", - "copyFailed": "No s'ha pogut copiar al porta-retalls" + "copyFailed": "No s'ha pogut copiar al porta-retalls", + "curlCopied": "Ordre cURL copiada" }, "layout": { "collections": "Col·leccions", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.exemple.com/v1/...", "sending": "S'està enviant...", "send": "Envia", - "invalidJsonBodyHelp": "No es pot enviar: el cos JSON no és vàlid" + "invalidJsonBodyHelp": "No es pot enviar: el cos JSON no és vàlid", + "copyCurl": "Copia com a cURL" }, "requestTabs": { "params": "Params", @@ -1915,7 +2087,10 @@ "placeholderName": "La meva petició", "labelFolder": "Carpeta", "placeholderFolder": "Selecciona una carpeta", - "save": "Desa" + "save": "Desa", + "newFolder": "Carpeta nova", + "newFolderPlaceholder": "Nom de la carpeta", + "create": "Crea" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Executant consulta…", "emptyTitle": "Executa una consulta per veure els resultats", "emptyHint": "Prem ⌘↩ o fes clic a Executa", - "toastNoConnection": "No hi ha connexió activa." + "toastNoConnection": "No hi ha connexió activa.", + "btnHistory": "Historial", + "btnSaveQuery": "Desa la consulta", + "savedSection": "Desades", + "recentSection": "Recents", + "emptyHistory": "Encara no hi ha res — executa o desa una consulta", + "savePlaceholder": "Nom de la consulta", + "deleteSaved": "Suprimeix", + "toastQuerySaved": "Consulta desada" }, "results": { "filterPlaceholder": "Filtra els resultats…", + "editHint": "Fes doble clic en una cel·la per editar", + "toastRowUpdated": "Fila actualitzada", + "toastRowsUpdated": "{count, plural, one {# fila actualitzada} other {# files actualitzades}}", "rowCount": "{count} {count, plural, one {fila} other {files}}", "rowCountFiltered": "{filtered} / {total} files", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/cs.json b/apps/desktop-ui/messages/cs.json index 877ccf87..d2d63a26 100644 --- a/apps/desktop-ui/messages/cs.json +++ b/apps/desktop-ui/messages/cs.json @@ -1580,7 +1580,13 @@ "cancel": "Zrušit", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Nepodařilo se smazat # kolekci} few {Nepodařilo se smazat # kolekce} many {Nepodařilo se smazat # kolekce} other {Nepodařilo se smazat # kolekcí}}", + "bulkDeleted": "{count, plural, one {Smazána # kolekce} few {Smazány # kolekce} many {Smazáno # kolekce} other {Smazáno # kolekcí}}", + "bulkDeleteButton": "{count, plural, one {Smazat # kolekci} few {Smazat # kolekce} many {Smazat # kolekce} other {Smazat # kolekcí}}", + "bulkDeleteFailed": "{count, plural, one {Nepodařilo se smazat # kolekci} few {Nepodařilo se smazat # kolekce} many {Nepodařilo se smazat # kolekce} other {Nepodařilo se smazat # kolekcí}}", + "bulkDeleted": "{count, plural, one {Smazána # kolekce} few {Smazány # kolekce} many {Smazáno # kolekce} other {Smazáno # kolekcí}}", + "bulkDeleteButton": "Smazat {count, plural, one {# kolekci} few {# kolekce} many {# kolekce} other {# kolekcí}}" }, "document": { "docsBreadcrumb": "{n} dok.", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} vráceno", "explainDocsExamined": "{n} prozkoumáno", "explainCollscanHint": "Tento dotaz prochází každý dokument v kolekci. Zvažte vytvoření indexu na filtrovaných polích.", - "explainRawLabel": "Výstup explain" + "explainRawLabel": "Výstup explain", + "docInsertFailed": "Nepodařilo se vložit dokument", + "docUpdateFailed": "Nepodařilo se aktualizovat dokument", + "indexesLoadFail": "Nepodařilo se načíst indexy", + "bulkDeleted": "{count, plural, one {# dokument smazán} few {# dokumenty smazány} many {# dokumentu smazáno} other {# dokumentů smazáno}}", + "bulkDeleteFailed": "Hromadné smazání se nezdařilo", + "selectedCount": "{count, plural, one {vybrán # dokument} few {vybrány # dokumenty} many {vybráno # dokumentu} other {vybráno # dokumentů}}", + "deleteSelected": "Smazat vybrané", + "clearSelection": "Vymazat", + "statusLoading": "Načítání…", + "statusShowing": "{from}–{to} z {total} dokumentů", + "statusEmpty": "0 dokumentů", + "statusSelected": "{count} vybráno", + "bulkDeleteTitle": "Smazat {count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}}?", + "bulkDeleteDescription": "Tímto trvale smažete {count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}} z {collection}. Tuto akci nelze vrátit zpět.", + "bulkDeleting": "Mazání…", + "bulkDeleteConfirm": "Smazat vše", + "queryErrorTitle": "Nepodařilo se načíst dokumenty", + "retry": "Zkusit znovu", + "docInsertFailed": "Nepodařilo se vložit dokument", + "docUpdateFailed": "Nepodařilo se aktualizovat dokument", + "indexesLoadFail": "Nepodařilo se načíst indexy", + "bulkDeleted": "{count, plural, one {# dokument smazán} few {# dokumenty smazány} many {# dokumentu smazáno} other {# dokumentů smazáno}}", + "bulkDeleteFailed": "Hromadné mazání selhalo", + "selectedCount": "{count, plural, one {# vybraný dokument} few {# vybrané dokumenty} many {# vybraného dokumentu} other {# vybraných dokumentů}}", + "deleteSelected": "Smazat vybrané", + "clearSelection": "Vymazat", + "statusLoading": "Načítání…", + "statusShowing": "Zobrazeno {from}–{to} z {total} dokumentů", + "statusEmpty": "0 dokumentů", + "statusSelected": "Vybráno: {count}", + "bulkDeleteTitle": "Smazat {count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}}?", + "bulkDeleteDescription": "Tímto trvale smažete {count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}} z {collection}. Tuto akci nelze vrátit zpět.", + "bulkDeleting": "Mazání…", + "bulkDeleteConfirm": "Smazat vše", + "queryErrorTitle": "Nepodařilo se načíst dokumenty", + "retry": "Zkusit znovu" }, "tabs": { "filterActive": "Aktivní filtr", @@ -1733,7 +1775,9 @@ "cancel": "Zrušit", "export": "Export", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exportuje pouze aktuální stránku ({count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}})", + "pageOnlyNote": "Exportuje pouze aktuální stránku ({count, plural, one {# dokument} few {# dokumenty} many {# dokumentu} other {# dokumentů}})" }, "jsonTree": { "typeLabel": "Typ: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Výsledky náhledu ({count})", "previewEmpty": "V této fázi nejsou žádné dokumenty", "previewFail": "Náhled se nezdařil" + }, + "indexManager": { + "loading": "Načítání indexů…", + "retry": "Zkusit znovu", + "countLabel": "{count, plural, one {# index} few {# indexy} many {# indexu} other {# indexů}}", + "totalSize": "celkem {size}", + "statsDocs": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "statsStorage": "{size} úložiště", + "statsAvgObj": "{size} prům./dok.", + "newIndex": "Nový index", + "createTitle": "Vytvořit index", + "fieldPlaceholder": "Název pole", + "ascending": "Vzestupně", + "descending": "Sestupně", + "addField": "Přidat pole", + "unique": "Unikátní", + "sparse": "Sparse", + "ttlLabel": "TTL (sekundy)", + "ttlPlaceholder": "např. 3600", + "cancel": "Zrušit", + "create": "Vytvořit", + "creating": "Vytváření…", + "created": "Index vytvořen", + "createFailed": "Nepodařilo se vytvořit index", + "fieldRequired": "Název pole je povinný", + "dropped": "Index „{name}“ smazán", + "dropFailed": "Nepodařilo se smazat index", + "dropTitle": "Smazat index?", + "dropDescription": "Tímto trvale smažete index „{name}“. Dotazy používající tento index budou pomalejší.", + "dropping": "Mazání…", + "dropConfirm": "Smazat index", + "badgeSystem": "systém", + "badgeUnique": "unikátní", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Nebyly nalezeny žádné indexy" + }, + "importDialog": { + "title": "Importovat dokumenty do {collection}", + "onlyJson": "Podporovány jsou pouze soubory .json", + "invalidStructure": "Soubor musí obsahovat pole nebo objekt JSON", + "invalidJson": "Neplatný JSON: soubor nelze zpracovat", + "imported": "{count, plural, one {Importován # dokument} few {Importovány # dokumenty} many {Importováno # dokumentu} other {Importováno # dokumentů}}", + "importFailed": "Import se nezdařil", + "dropHint": "Přetáhněte nebo klikněte pro nahrání", + "dropSubHint": "Podporuje pole JSON nebo NDJSON", + "docsCount": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "previewLabel": "Náhled (první 3 dokumenty)", + "moreDocs": "… a {count} dalších dokumentů", + "cancel": "Zrušit", + "importing": "Import…", + "importCount": "{count, plural, one {Importovat # dok.} few {Importovat # dok.} many {Importovat # dok.} other {Importovat # dok.}}", + "importBtn": "Importovat" + }, + "schemaView": { + "analyzing": "Analýza schématu…", + "loadFailed": "Nepodařilo se analyzovat schéma", + "retry": "Zkusit znovu", + "noDocs": "Žádné dokumenty k analýze", + "sampled": "Vzorkováno {docs} dokumentů · {fields} polí", + "colField": "Pole", + "colTypes": "Typy", + "colCoverage": "Pokrytí" + }, + "indexManager": { + "loading": "Načítání indexů…", + "retry": "Zkusit znovu", + "countLabel": "{count, plural, one {# index} few {# indexy} many {# indexu} other {# indexů}}", + "totalSize": "{size} celkem", + "statsDocs": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "statsStorage": "{size} úložiště", + "statsAvgObj": "{size} prům./dok.", + "newIndex": "Nový index", + "createTitle": "Vytvořit index", + "fieldPlaceholder": "Název pole", + "ascending": "Vzestupně", + "descending": "Sestupně", + "addField": "Přidat pole", + "unique": "Jedinečný", + "sparse": "Řídký", + "ttlLabel": "TTL (sekundy)", + "ttlPlaceholder": "např. 3600", + "cancel": "Zrušit", + "create": "Vytvořit", + "creating": "Vytváření…", + "created": "Index vytvořen", + "createFailed": "Nepodařilo se vytvořit index", + "fieldRequired": "Název pole je povinný", + "dropped": "Index \"{name}\" byl odstraněn", + "dropFailed": "Nepodařilo se odstranit index", + "dropTitle": "Odstranit index?", + "dropDescription": "Tímto trvale odstraníte index \"{name}\". Dotazy využívající tento index budou pomalejší.", + "dropping": "Odstraňování…", + "dropConfirm": "Odstranit index", + "badgeSystem": "systém", + "badgeUnique": "jedinečný", + "badgeSparse": "řídký", + "badgeTtl": "TTL", + "empty": "Nebyly nalezeny žádné indexy" + }, + "importDialog": { + "title": "Importovat dokumenty do {collection}", + "onlyJson": "Podporovány jsou pouze soubory .json", + "invalidStructure": "Soubor musí obsahovat pole nebo objekt JSON", + "invalidJson": "Neplatný JSON: soubor se nepodařilo zpracovat", + "imported": "{count, plural, one {Importován # dokument} few {Importovány # dokumenty} many {Importováno # dokumentu} other {Importováno # dokumentů}}", + "importFailed": "Import selhal", + "dropHint": "Přetáhněte nebo klikněte pro nahrání", + "dropSubHint": "Podporuje pole JSON nebo NDJSON", + "docsCount": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "previewLabel": "Náhled (první 3 dokumenty)", + "moreDocs": "… a {count} dalších dokumentů", + "cancel": "Zrušit", + "importing": "Importování…", + "importCount": "{count, plural, one {Importovat # dok.} few {Importovat # dok.} many {Importovat # dok.} other {Importovat # dok.}}", + "importBtn": "Importovat" + }, + "schemaView": { + "analyzing": "Analýza schématu…", + "loadFailed": "Nepodařilo se analyzovat schéma", + "retry": "Zkusit znovu", + "noDocs": "Žádné dokumenty k analýze", + "sampled": "Vzorkováno {docs} dokumentů · {fields} polí", + "colField": "Pole", + "colTypes": "Typy", + "colCoverage": "Pokrytí" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL vložen a zpracován", "responseCopied": "Odpověď zkopírována", "codeCopied": "Kód zkopírován", - "copyFailed": "Nepodařilo se zkopírovat do schránky" + "copyFailed": "Nepodařilo se zkopírovat do schránky", + "curlCopied": "Příkaz cURL zkopírován" }, "layout": { "collections": "Kolekce", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Odesílání...", "send": "Odeslat", - "invalidJsonBodyHelp": "Nelze odeslat: tělo JSON není platné" + "invalidJsonBodyHelp": "Nelze odeslat: tělo JSON není platné", + "copyCurl": "Kopírovat jako cURL" }, "requestTabs": { "params": "Parametry", @@ -1915,7 +2087,10 @@ "placeholderName": "Můj požadavek", "labelFolder": "Složka", "placeholderFolder": "Vyberte složku", - "save": "Uložit" + "save": "Uložit", + "newFolder": "Nová složka", + "newFolderPlaceholder": "Název složky", + "create": "Vytvořit" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Provádí se dotaz…", "emptyTitle": "Spusťte dotaz pro zobrazení výsledků", "emptyHint": "Stiskněte ⌘↩ nebo klikněte Spustit", - "toastNoConnection": "Žádné aktivní připojení." + "toastNoConnection": "Žádné aktivní připojení.", + "btnHistory": "Historie", + "btnSaveQuery": "Uložit dotaz", + "savedSection": "Uložené", + "recentSection": "Nedávné", + "emptyHistory": "Zatím nic — spusťte nebo uložte dotaz", + "savePlaceholder": "Název dotazu", + "deleteSaved": "Smazat", + "toastQuerySaved": "Dotaz uložen" }, "results": { "filterPlaceholder": "Filtrovat výsledky…", + "editHint": "Dvojklikem na buňku upravíte", + "toastRowUpdated": "Řádek aktualizován", + "toastRowsUpdated": "{count, plural, one {# řádek aktualizován} few {# řádky aktualizovány} many {# řádku aktualizováno} other {# řádků aktualizováno}}", "rowCount": "{count} {count, plural, one {řádek} few {řádky} other {řádků}}", "rowCountFiltered": "{filtered} / {total} řádků", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/da.json b/apps/desktop-ui/messages/da.json index 3c56a720..d2020c77 100644 --- a/apps/desktop-ui/messages/da.json +++ b/apps/desktop-ui/messages/da.json @@ -1580,7 +1580,13 @@ "cancel": "Ophæve", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Kunne ikke slette # samling} other {Kunne ikke slette # samlinger}}", + "bulkDeleted": "{count, plural, one {# samling slettet} other {# samlinger slettet}}", + "bulkDeleteButton": "{count, plural, one {Slet # samling} other {Slet # samlinger}}", + "bulkDeleteFailed": "{count, plural, one {Kunne ikke slette # samling} other {Kunne ikke slette # samlinger}}", + "bulkDeleted": "{count, plural, one {Slettede # samling} other {Slettede # samlinger}}", + "bulkDeleteButton": "Slet {count, plural, one {# samling} other {# samlinger}}" }, "document": { "docsBreadcrumb": "{n} dokumenter", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} returneret", "explainDocsExamined": "{n} undersøgt", "explainCollscanHint": "Denne forespørgsel scanner alle dokumenter i collectionen. Overvej at oprette et indeks på de filtrerede felter.", - "explainRawLabel": "Explain-output" + "explainRawLabel": "Explain-output", + "docInsertFailed": "Kunne ikke indsætte dokument", + "docUpdateFailed": "Kunne ikke opdatere dokument", + "indexesLoadFail": "Kunne ikke indlæse indekser", + "bulkDeleted": "{count, plural, one {# dokument slettet} other {# dokumenter slettet}}", + "bulkDeleteFailed": "Massesletning mislykkedes", + "selectedCount": "{count, plural, one {# dokument valgt} other {# dokumenter valgt}}", + "deleteSelected": "Slet valgte", + "clearSelection": "Ryd", + "statusLoading": "Indlæser…", + "statusShowing": "{from}-{to} af {total} dokumenter", + "statusEmpty": "0 dokumenter", + "statusSelected": "{count} valgt", + "bulkDeleteTitle": "Slet {count, plural, one {# dokument} other {# dokumenter}}?", + "bulkDeleteDescription": "Dette sletter permanent {count, plural, one {# dokument} other {# dokumenter}} fra {collection}. Dette kan ikke fortrydes.", + "bulkDeleting": "Sletter…", + "bulkDeleteConfirm": "Slet alle", + "queryErrorTitle": "Kunne ikke indlæse dokumenter", + "retry": "Prøv igen", + "docInsertFailed": "Dokumentet kunne ikke indsættes", + "docUpdateFailed": "Dokumentet kunne ikke opdateres", + "indexesLoadFail": "Indekserne kunne ikke indlæses", + "bulkDeleted": "{count, plural, one {# dokument slettet} other {# dokumenter slettet}}", + "bulkDeleteFailed": "Massesletning mislykkedes", + "selectedCount": "{count, plural, one {# dokument valgt} other {# dokumenter valgt}}", + "deleteSelected": "Slet valgte", + "clearSelection": "Ryd", + "statusLoading": "Indlæser …", + "statusShowing": "Viser {from}-{to} af {total} dokumenter", + "statusEmpty": "0 dokumenter", + "statusSelected": "{count} valgt", + "bulkDeleteTitle": "Slet {count, plural, one {# dokument} other {# dokumenter}}?", + "bulkDeleteDescription": "Dette sletter permanent {count, plural, one {# dokument} other {# dokumenter}} fra {collection}. Dette kan ikke fortrydes.", + "bulkDeleting": "Sletter …", + "bulkDeleteConfirm": "Slet alle", + "queryErrorTitle": "Dokumenterne kunne ikke indlæses", + "retry": "Prøv igen" }, "tabs": { "filterActive": "Aktivt filter anvendt", @@ -1733,7 +1775,9 @@ "cancel": "Ophæve", "export": "Eksportere", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Eksporterer kun den aktuelle side ({count, plural, one {# dokument} other {# dokumenter}})", + "pageOnlyNote": "Eksporterer kun den aktuelle side ({count, plural, one {# dokument} other {# dokumenter}})" }, "jsonTree": { "typeLabel": "Type: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Forhåndsvisningsresultater ({count})", "previewEmpty": "Ingen dokumenter på dette trin", "previewFail": "Forhåndsvisning mislykkedes" + }, + "indexManager": { + "loading": "Indlæser indekser…", + "retry": "Prøv igen", + "countLabel": "{count, plural, one {# indeks} other {# indekser}}", + "totalSize": "{size} i alt", + "statsDocs": "{count, plural, one {# dok.} other {# dok.}}", + "statsStorage": "{size} lager", + "statsAvgObj": "{size} gns./dok.", + "newIndex": "Nyt indeks", + "createTitle": "Opret indeks", + "fieldPlaceholder": "Feltnavn", + "ascending": "Stigende", + "descending": "Faldende", + "addField": "Tilføj felt", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (sekunder)", + "ttlPlaceholder": "f.eks. 3600", + "cancel": "Annuller", + "create": "Opret", + "creating": "Opretter…", + "created": "Indeks oprettet", + "createFailed": "Kunne ikke oprette indeks", + "fieldRequired": "Feltnavn er påkrævet", + "dropped": "Indeks \"{name}\" slettet", + "dropFailed": "Kunne ikke slette indeks", + "dropTitle": "Slet indeks?", + "dropDescription": "Dette sletter permanent indekset \"{name}\". Forespørgsler, der bruger dette indeks, bliver langsommere.", + "dropping": "Sletter…", + "dropConfirm": "Slet indeks", + "badgeSystem": "system", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Ingen indekser fundet" + }, + "importDialog": { + "title": "Importér dokumenter til {collection}", + "onlyJson": "Kun .json-filer understøttes", + "invalidStructure": "Filen skal indeholde et JSON-array eller -objekt", + "invalidJson": "Ugyldig JSON: filen kunne ikke parses", + "imported": "{count, plural, one {# dokument importeret} other {# dokumenter importeret}}", + "importFailed": "Import mislykkedes", + "dropHint": "Træk og slip eller klik for at uploade", + "dropSubHint": "Understøtter JSON-array eller NDJSON", + "docsCount": "{count, plural, one {# dok.} other {# dok.}}", + "previewLabel": "Forhåndsvisning (første 3 dokumenter)", + "moreDocs": "… og {count} dokumenter mere", + "cancel": "Annuller", + "importing": "Importerer…", + "importCount": "{count, plural, one {Importér # dok.} other {Importér # dok.}}", + "importBtn": "Importér" + }, + "schemaView": { + "analyzing": "Analyserer skema…", + "loadFailed": "Kunne ikke analysere skema", + "retry": "Prøv igen", + "noDocs": "Ingen dokumenter at analysere", + "sampled": "{docs} dokumenter analyseret · {fields} felter", + "colField": "Felt", + "colTypes": "Typer", + "colCoverage": "Dækning" + }, + "indexManager": { + "loading": "Indlæser indekser …", + "retry": "Prøv igen", + "countLabel": "{count, plural, one {# indeks} other {# indekser}}", + "totalSize": "{size} i alt", + "statsDocs": "{count, plural, one {# dok.} other {# dok.}}", + "statsStorage": "{size} lager", + "statsAvgObj": "{size} gns./dok.", + "newIndex": "Nyt indeks", + "createTitle": "Opret indeks", + "fieldPlaceholder": "Feltnavn", + "ascending": "Stigende", + "descending": "Faldende", + "addField": "Tilføj felt", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (sekunder)", + "ttlPlaceholder": "f.eks. 3600", + "cancel": "Annuller", + "create": "Opret", + "creating": "Opretter …", + "created": "Indeks oprettet", + "createFailed": "Indekset kunne ikke oprettes", + "fieldRequired": "Feltnavn er påkrævet", + "dropped": "Indeks \"{name}\" fjernet", + "dropFailed": "Indekset kunne ikke fjernes", + "dropTitle": "Fjern indeks?", + "dropDescription": "Dette fjerner permanent indekset \"{name}\". Forespørgsler, der bruger dette indeks, bliver langsommere.", + "dropping": "Fjerner …", + "dropConfirm": "Fjern indeks", + "badgeSystem": "system", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Ingen indekser fundet" + }, + "importDialog": { + "title": "Importér dokumenter til {collection}", + "onlyJson": "Kun .json-filer understøttes", + "invalidStructure": "Filen skal indeholde et JSON-array eller -objekt", + "invalidJson": "Ugyldig JSON: filen kunne ikke parses", + "imported": "{count, plural, one {Importerede # dokument} other {Importerede # dokumenter}}", + "importFailed": "Import mislykkedes", + "dropHint": "Træk og slip eller klik for at uploade", + "dropSubHint": "Understøtter JSON-array eller NDJSON", + "docsCount": "{count, plural, one {# dok.} other {# dok.}}", + "previewLabel": "Forhåndsvisning (første 3 dokumenter)", + "moreDocs": "… og {count} dokumenter mere", + "cancel": "Annuller", + "importing": "Importerer …", + "importCount": "{count, plural, one {Importér # dok.} other {Importér # dok.}}", + "importBtn": "Importér" + }, + "schemaView": { + "analyzing": "Analyserer skema …", + "loadFailed": "Skemaet kunne ikke analyseres", + "retry": "Prøv igen", + "noDocs": "Ingen dokumenter at analysere", + "sampled": "Stikprøve af {docs} dokumenter · {fields} felter", + "colField": "Felt", + "colTypes": "Typer", + "colCoverage": "Dækning" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL blev indsat og parset", "responseCopied": "Svar kopieret til udklipsholder", "codeCopied": "Kode kopieret til udklipsholder", - "copyFailed": "Kopiering til udklipsholder mislykkedes" + "copyFailed": "Kopiering til udklipsholder mislykkedes", + "curlCopied": "cURL-kommando kopieret" }, "layout": { "collections": "Samlinger", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sender...", "send": "Sende", - "invalidJsonBodyHelp": "Kan ikke sende: JSON-brødteksten er ugyldig" + "invalidJsonBodyHelp": "Kan ikke sende: JSON-brødteksten er ugyldig", + "copyCurl": "Kopiér som cURL" }, "requestTabs": { "params": "Params", @@ -1915,7 +2087,10 @@ "placeholderName": "Min anmodning", "labelFolder": "Folder", "placeholderFolder": "Vælg en mappe", - "save": "Spare" + "save": "Spare", + "newFolder": "Ny mappe", + "newFolderPlaceholder": "Mappenavn", + "create": "Opret" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Udfører forespørgsel…", "emptyTitle": "Kør en forespørgsel for at se resultater", "emptyHint": "Tryk ⌘↩ eller klik Kør", - "toastNoConnection": "Ingen aktiv forbindelse." + "toastNoConnection": "Ingen aktiv forbindelse.", + "btnHistory": "Historik", + "btnSaveQuery": "Gem forespørgsel", + "savedSection": "Gemte", + "recentSection": "Seneste", + "emptyHistory": "Intet endnu — kør eller gem en forespørgsel", + "savePlaceholder": "Forespørgselsnavn", + "deleteSaved": "Slet", + "toastQuerySaved": "Forespørgsel gemt" }, "results": { "filterPlaceholder": "Filtrer resultater…", + "editHint": "Dobbeltklik på en celle for at redigere", + "toastRowUpdated": "Række opdateret", + "toastRowsUpdated": "{count, plural, one {# række opdateret} other {# rækker opdateret}}", "rowCount": "{count} {count, plural, one {række} other {rækker}}", "rowCountFiltered": "{filtered} / {total} rækker", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/de.json b/apps/desktop-ui/messages/de.json index e46b125e..6c29a90c 100644 --- a/apps/desktop-ui/messages/de.json +++ b/apps/desktop-ui/messages/de.json @@ -1580,7 +1580,13 @@ "cancel": "Abbrechen", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {# Sammlung konnte nicht gelöscht werden} other {# Sammlungen konnten nicht gelöscht werden}}", + "bulkDeleted": "{count, plural, one {# Sammlung gelöscht} other {# Sammlungen gelöscht}}", + "bulkDeleteButton": "{count, plural, one {# Sammlung löschen} other {# Sammlungen löschen}}", + "bulkDeleteFailed": "{count, plural, one {# Sammlung konnte nicht gelöscht werden} other {# Sammlungen konnten nicht gelöscht werden}}", + "bulkDeleted": "{count, plural, one {# Sammlung gelöscht} other {# Sammlungen gelöscht}}", + "bulkDeleteButton": "{count, plural, one {# Sammlung} other {# Sammlungen}} löschen" }, "document": { "docsBreadcrumb": "{n} Dok.", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} zurückgegeben", "explainDocsExamined": "{n} untersucht", "explainCollscanHint": "Diese Abfrage durchsucht jedes Dokument der Collection. Erwäge einen Index auf den gefilterten Feldern.", - "explainRawLabel": "Explain-Ausgabe" + "explainRawLabel": "Explain-Ausgabe", + "docInsertFailed": "Dokument konnte nicht eingefügt werden", + "docUpdateFailed": "Dokument konnte nicht aktualisiert werden", + "indexesLoadFail": "Indizes konnten nicht geladen werden", + "bulkDeleted": "{count, plural, one {# Dokument gelöscht} other {# Dokumente gelöscht}}", + "bulkDeleteFailed": "Massenlöschung fehlgeschlagen", + "selectedCount": "{count, plural, one {# Dokument ausgewählt} other {# Dokumente ausgewählt}}", + "deleteSelected": "Ausgewählte löschen", + "clearSelection": "Leeren", + "statusLoading": "Wird geladen …", + "statusShowing": "{from}–{to} von {total} Dokumenten", + "statusEmpty": "0 Dokumente", + "statusSelected": "{count} ausgewählt", + "bulkDeleteTitle": "{count, plural, one {# Dokument} other {# Dokumente}} löschen?", + "bulkDeleteDescription": "Dadurch werden {count, plural, one {# Dokument} other {# Dokumente}} dauerhaft aus {collection} gelöscht. Dies kann nicht rückgängig gemacht werden.", + "bulkDeleting": "Wird gelöscht …", + "bulkDeleteConfirm": "Alle löschen", + "queryErrorTitle": "Dokumente konnten nicht geladen werden", + "retry": "Erneut versuchen", + "docInsertFailed": "Dokument konnte nicht eingefügt werden", + "docUpdateFailed": "Dokument konnte nicht aktualisiert werden", + "indexesLoadFail": "Indizes konnten nicht geladen werden", + "bulkDeleted": "{count, plural, one {# Dokument gelöscht} other {# Dokumente gelöscht}}", + "bulkDeleteFailed": "Massenlöschung fehlgeschlagen", + "selectedCount": "{count, plural, one {# Dokument ausgewählt} other {# Dokumente ausgewählt}}", + "deleteSelected": "Auswahl löschen", + "clearSelection": "Leeren", + "statusLoading": "Wird geladen …", + "statusShowing": "{from}–{to} von {total} Dokumenten", + "statusEmpty": "0 Dokumente", + "statusSelected": "{count} ausgewählt", + "bulkDeleteTitle": "{count, plural, one {# Dokument} other {# Dokumente}} löschen?", + "bulkDeleteDescription": "Dadurch werden {count, plural, one {# Dokument} other {# Dokumente}} dauerhaft aus {collection} gelöscht. Dies kann nicht rückgängig gemacht werden.", + "bulkDeleting": "Wird gelöscht …", + "bulkDeleteConfirm": "Alle löschen", + "queryErrorTitle": "Dokumente konnten nicht geladen werden", + "retry": "Erneut versuchen" }, "tabs": { "filterActive": "Filter aktiv", @@ -1733,7 +1775,9 @@ "cancel": "Abbrechen", "export": "Exportieren", "sheetName": "Daten", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exportiert nur die aktuelle Seite ({count, plural, one {# Dokument} other {# Dokumente}})", + "pageOnlyNote": "Exportiert nur die aktuelle Seite ({count, plural, one {# Dokument} other {# Dokumente}})" }, "jsonTree": { "typeLabel": "Typ: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Vorschauergebnisse ({count})", "previewEmpty": "Keine Dokumente in dieser Stufe", "previewFail": "Vorschau fehlgeschlagen" + }, + "indexManager": { + "loading": "Indizes werden geladen …", + "retry": "Erneut versuchen", + "countLabel": "{count, plural, one {# Index} other {# Indizes}}", + "totalSize": "{size} gesamt", + "statsDocs": "{count, plural, one {# Dok.} other {# Dok.}}", + "statsStorage": "{size} Speicher", + "statsAvgObj": "{size} Ø/Dok.", + "newIndex": "Neuer Index", + "createTitle": "Index erstellen", + "fieldPlaceholder": "Feldname", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "addField": "Feld hinzufügen", + "unique": "Eindeutig", + "sparse": "Sparse", + "ttlLabel": "TTL (Sekunden)", + "ttlPlaceholder": "z. B. 3600", + "cancel": "Abbrechen", + "create": "Erstellen", + "creating": "Wird erstellt …", + "created": "Index erstellt", + "createFailed": "Index konnte nicht erstellt werden", + "fieldRequired": "Feldname ist erforderlich", + "dropped": "Index \"{name}\" gelöscht", + "dropFailed": "Index konnte nicht gelöscht werden", + "dropTitle": "Index löschen?", + "dropDescription": "Dadurch wird der Index \"{name}\" dauerhaft gelöscht. Abfragen, die diesen Index verwenden, werden langsamer.", + "dropping": "Wird gelöscht …", + "dropConfirm": "Index löschen", + "badgeSystem": "System", + "badgeUnique": "eindeutig", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Keine Indizes gefunden" + }, + "importDialog": { + "title": "Dokumente in {collection} importieren", + "onlyJson": "Nur .json-Dateien werden unterstützt", + "invalidStructure": "Datei muss ein JSON-Array oder -Objekt enthalten", + "invalidJson": "Ungültiges JSON: Datei konnte nicht geparst werden", + "imported": "{count, plural, one {# Dokument importiert} other {# Dokumente importiert}}", + "importFailed": "Import fehlgeschlagen", + "dropHint": "Zum Hochladen ziehen & ablegen oder klicken", + "dropSubHint": "Unterstützt JSON-Array oder NDJSON", + "docsCount": "{count, plural, one {# Dok.} other {# Dok.}}", + "previewLabel": "Vorschau (erste 3 Dokumente)", + "moreDocs": "… und {count} weitere Dokumente", + "cancel": "Abbrechen", + "importing": "Wird importiert …", + "importCount": "{count, plural, one {# Dok. importieren} other {# Dok. importieren}}", + "importBtn": "Importieren" + }, + "schemaView": { + "analyzing": "Schema wird analysiert …", + "loadFailed": "Schema konnte nicht analysiert werden", + "retry": "Erneut versuchen", + "noDocs": "Keine Dokumente zum Analysieren", + "sampled": "{docs} Dokumente untersucht · {fields} Felder", + "colField": "Feld", + "colTypes": "Typen", + "colCoverage": "Abdeckung" + }, + "indexManager": { + "loading": "Indizes werden geladen …", + "retry": "Erneut versuchen", + "countLabel": "{count, plural, one {# Index} other {# Indizes}}", + "totalSize": "{size} gesamt", + "statsDocs": "{count, plural, one {# Dok.} other {# Dok.}}", + "statsStorage": "{size} Speicher", + "statsAvgObj": "{size} Ø/Dok.", + "newIndex": "Neuer Index", + "createTitle": "Index erstellen", + "fieldPlaceholder": "Feldname", + "ascending": "Aufsteigend", + "descending": "Absteigend", + "addField": "Feld hinzufügen", + "unique": "Eindeutig", + "sparse": "Sparse", + "ttlLabel": "TTL (Sekunden)", + "ttlPlaceholder": "z. B. 3600", + "cancel": "Abbrechen", + "create": "Erstellen", + "creating": "Wird erstellt …", + "created": "Index erstellt", + "createFailed": "Index konnte nicht erstellt werden", + "fieldRequired": "Feldname ist erforderlich", + "dropped": "Index \"{name}\" entfernt", + "dropFailed": "Index konnte nicht entfernt werden", + "dropTitle": "Index entfernen?", + "dropDescription": "Dadurch wird der Index \"{name}\" dauerhaft entfernt. Abfragen, die diesen Index verwenden, werden langsamer.", + "dropping": "Wird entfernt …", + "dropConfirm": "Index entfernen", + "badgeSystem": "System", + "badgeUnique": "eindeutig", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Keine Indizes gefunden" + }, + "importDialog": { + "title": "Dokumente in {collection} importieren", + "onlyJson": "Nur .json-Dateien werden unterstützt", + "invalidStructure": "Die Datei muss ein JSON-Array oder -Objekt enthalten", + "invalidJson": "Ungültiges JSON: Datei konnte nicht geparst werden", + "imported": "{count, plural, one {# Dokument importiert} other {# Dokumente importiert}}", + "importFailed": "Import fehlgeschlagen", + "dropHint": "Zum Hochladen ziehen und ablegen oder klicken", + "dropSubHint": "Unterstützt JSON-Array oder NDJSON", + "docsCount": "{count, plural, one {# Dok.} other {# Dok.}}", + "previewLabel": "Vorschau (erste 3 Dokumente)", + "moreDocs": "… und {count} weitere Dokumente", + "cancel": "Abbrechen", + "importing": "Wird importiert …", + "importCount": "{count, plural, one {# Dok. importieren} other {# Dok. importieren}}", + "importBtn": "Importieren" + }, + "schemaView": { + "analyzing": "Schema wird analysiert …", + "loadFailed": "Schema konnte nicht analysiert werden", + "retry": "Erneut versuchen", + "noDocs": "Keine Dokumente zum Analysieren", + "sampled": "{docs} Dokumente untersucht · {fields} Felder", + "colField": "Feld", + "colTypes": "Typen", + "colCoverage": "Abdeckung" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL eingefügt und erfolgreich gelesen", "responseCopied": "Antwort in die Zwischenablage kopiert", "codeCopied": "Code in die Zwischenablage kopiert", - "copyFailed": "In die Zwischenablage kopieren fehlgeschlagen" + "copyFailed": "In die Zwischenablage kopieren fehlgeschlagen", + "curlCopied": "cURL-Befehl kopiert" }, "layout": { "collections": "Sammlungen", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Senden...", "send": "Senden", - "invalidJsonBodyHelp": "Senden nicht möglich: Der JSON-Body ist ungültig" + "invalidJsonBodyHelp": "Senden nicht möglich: Der JSON-Body ist ungültig", + "copyCurl": "Als cURL kopieren" }, "requestTabs": { "params": "Parameter", @@ -1915,7 +2087,10 @@ "placeholderName": "Meine Anfrage", "labelFolder": "Ordner", "placeholderFolder": "Ordner wählen", - "save": "Speichern" + "save": "Speichern", + "newFolder": "Neuer Ordner", + "newFolderPlaceholder": "Ordnername", + "create": "Erstellen" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Abfrage wird ausgeführt…", "emptyTitle": "Abfrage ausführen, um Ergebnisse zu sehen", "emptyHint": "⌘↩ drücken oder Ausführen klicken", - "toastNoConnection": "Keine aktive Verbindung." + "toastNoConnection": "Keine aktive Verbindung.", + "btnHistory": "Verlauf", + "btnSaveQuery": "Abfrage speichern", + "savedSection": "Gespeichert", + "recentSection": "Zuletzt", + "emptyHistory": "Noch nichts hier — Abfrage ausführen oder speichern", + "savePlaceholder": "Abfragename", + "deleteSaved": "Löschen", + "toastQuerySaved": "Abfrage gespeichert" }, "results": { "filterPlaceholder": "Ergebnisse filtern…", + "editHint": "Zum Bearbeiten Zelle doppelklicken", + "toastRowUpdated": "Zeile aktualisiert", + "toastRowsUpdated": "{count, plural, one {# Zeile aktualisiert} other {# Zeilen aktualisiert}}", "rowCount": "{count} {count, plural, one {Zeile} other {Zeilen}}", "rowCountFiltered": "{filtered} / {total} Zeilen", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/el.json b/apps/desktop-ui/messages/el.json index 2d48fdad..7c76d021 100644 --- a/apps/desktop-ui/messages/el.json +++ b/apps/desktop-ui/messages/el.json @@ -1580,7 +1580,13 @@ "cancel": "Ματαίωση", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Αποτυχία διαγραφής # συλλογής} other {Αποτυχία διαγραφής # συλλογών}}", + "bulkDeleted": "{count, plural, one {# συλλογή διαγράφηκε} other {# συλλογές διαγράφηκαν}}", + "bulkDeleteButton": "{count, plural, one {Διαγραφή # συλλογής} other {Διαγραφή # συλλογών}}", + "bulkDeleteFailed": "{count, plural, one {Αποτυχία διαγραφής # συλλογής} other {Αποτυχία διαγραφής # συλλογών}}", + "bulkDeleted": "{count, plural, one {Διαγράφηκε # συλλογή} other {Διαγράφηκαν # συλλογές}}", + "bulkDeleteButton": "Διαγραφή {count, plural, one {# συλλογής} other {# συλλογών}}" }, "document": { "docsBreadcrumb": "{n} έγγραφα", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} επιστράφηκαν", "explainDocsExamined": "{n} εξετάστηκαν", "explainCollscanHint": "Αυτό το ερώτημα σαρώνει κάθε έγγραφο της συλλογής. Εξετάστε τη δημιουργία ευρετηρίου στα φιλτραρισμένα πεδία.", - "explainRawLabel": "Έξοδος explain" + "explainRawLabel": "Έξοδος explain", + "docInsertFailed": "Αποτυχία εισαγωγής εγγράφου", + "docUpdateFailed": "Αποτυχία ενημέρωσης εγγράφου", + "indexesLoadFail": "Αποτυχία φόρτωσης ευρετηρίων", + "bulkDeleted": "{count, plural, one {# έγγραφο διαγράφηκε} other {# έγγραφα διαγράφηκαν}}", + "bulkDeleteFailed": "Αποτυχία μαζικής διαγραφής", + "selectedCount": "{count, plural, one {# έγγραφο επιλέχθηκε} other {# έγγραφα επιλέχθηκαν}}", + "deleteSelected": "Διαγραφή επιλεγμένων", + "clearSelection": "Εκκαθάριση", + "statusLoading": "Φόρτωση…", + "statusShowing": "{from}–{to} από {total} έγγραφα", + "statusEmpty": "0 έγγραφα", + "statusSelected": "{count} επιλεγμένα", + "bulkDeleteTitle": "Διαγραφή {count, plural, one {# εγγράφου} other {# εγγράφων}};", + "bulkDeleteDescription": "Αυτό θα διαγράψει οριστικά {count, plural, one {# έγγραφο} other {# έγγραφα}} από {collection}. Δεν είναι δυνατή η αναίρεση.", + "bulkDeleting": "Διαγραφή…", + "bulkDeleteConfirm": "Διαγραφή όλων", + "queryErrorTitle": "Αποτυχία φόρτωσης εγγράφων", + "retry": "Επανάληψη", + "docInsertFailed": "Αποτυχία εισαγωγής εγγράφου", + "docUpdateFailed": "Αποτυχία ενημέρωσης εγγράφου", + "indexesLoadFail": "Αποτυχία φόρτωσης ευρετηρίων", + "bulkDeleted": "{count, plural, one {Διαγράφηκε # έγγραφο} other {Διαγράφηκαν # έγγραφα}}", + "bulkDeleteFailed": "Η μαζική διαγραφή απέτυχε", + "selectedCount": "{count, plural, one {# έγγραφο επιλέχθηκε} other {# έγγραφα επιλέχθηκαν}}", + "deleteSelected": "Διαγραφή επιλεγμένων", + "clearSelection": "Εκκαθάριση", + "statusLoading": "Φόρτωση...", + "statusShowing": "Εμφάνιση {from}-{to} από {total} έγγραφα", + "statusEmpty": "0 έγγραφα", + "statusSelected": "{count} επιλεγμένα", + "bulkDeleteTitle": "Διαγραφή {count, plural, one {# εγγράφου} other {# εγγράφων}};", + "bulkDeleteDescription": "Αυτό θα διαγράψει οριστικά {count, plural, one {# έγγραφο} other {# έγγραφα}} από τη συλλογή {collection}. Δεν είναι δυνατή η αναίρεση.", + "bulkDeleting": "Διαγραφή...", + "bulkDeleteConfirm": "Διαγραφή όλων", + "queryErrorTitle": "Αποτυχία φόρτωσης εγγράφων", + "retry": "Επανάληψη" }, "tabs": { "filterActive": "Εφαρμόστηκε ενεργό φίλτρο", @@ -1733,7 +1775,9 @@ "cancel": "Ματαίωση", "export": "Εξαγωγή", "sheetName": "Δεδομένα", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Εξάγει μόνο την τρέχουσα σελίδα ({count, plural, one {# έγγραφο} other {# έγγραφα}})", + "pageOnlyNote": "Εξάγει μόνο την τρέχουσα σελίδα ({count, plural, one {# έγγραφο} other {# έγγραφα}})" }, "jsonTree": { "typeLabel": "Τύπος: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Αποτελέσματα προεπισκόπησης ({count})", "previewEmpty": "Κανένα έγγραφο σε αυτό το στάδιο", "previewFail": "Αποτυχία προεπισκόπησης" + }, + "indexManager": { + "loading": "Φόρτωση ευρετηρίων…", + "retry": "Επανάληψη", + "countLabel": "{count, plural, one {# ευρετήριο} other {# ευρετήρια}}", + "totalSize": "{size} συνολικά", + "statsDocs": "{count, plural, one {# έγγρ.} other {# έγγρ.}}", + "statsStorage": "{size} αποθήκευση", + "statsAvgObj": "{size} μέσος όρος/έγγρ.", + "newIndex": "Νέο ευρετήριο", + "createTitle": "Δημιουργία ευρετηρίου", + "fieldPlaceholder": "Όνομα πεδίου", + "ascending": "Αύξουσα", + "descending": "Φθίνουσα", + "addField": "Προσθήκη πεδίου", + "unique": "Μοναδικό", + "sparse": "Sparse", + "ttlLabel": "TTL (δευτερόλεπτα)", + "ttlPlaceholder": "π.χ. 3600", + "cancel": "Άκυρο", + "create": "Δημιουργία", + "creating": "Δημιουργία…", + "created": "Το ευρετήριο δημιουργήθηκε", + "createFailed": "Αποτυχία δημιουργίας ευρετηρίου", + "fieldRequired": "Το όνομα πεδίου είναι υποχρεωτικό", + "dropped": "Το ευρετήριο \"{name}\" διαγράφηκε", + "dropFailed": "Αποτυχία διαγραφής ευρετηρίου", + "dropTitle": "Διαγραφή ευρετηρίου;", + "dropDescription": "Αυτό θα διαγράψει οριστικά το ευρετήριο \"{name}\". Τα ερωτήματα που το χρησιμοποιούν θα γίνουν πιο αργά.", + "dropping": "Διαγραφή…", + "dropConfirm": "Διαγραφή ευρετηρίου", + "badgeSystem": "σύστημα", + "badgeUnique": "μοναδικό", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Δεν βρέθηκαν ευρετήρια" + }, + "importDialog": { + "title": "Εισαγωγή εγγράφων στο {collection}", + "onlyJson": "Υποστηρίζονται μόνο αρχεία .json", + "invalidStructure": "Το αρχείο πρέπει να περιέχει πίνακα ή αντικείμενο JSON", + "invalidJson": "Μη έγκυρο JSON: δεν ήταν δυνατή η ανάλυση του αρχείου", + "imported": "{count, plural, one {Εισήχθη # έγγραφο} other {Εισήχθησαν # έγγραφα}}", + "importFailed": "Η εισαγωγή απέτυχε", + "dropHint": "Σύρετε και αποθέστε ή κάντε κλικ για μεταφόρτωση", + "dropSubHint": "Υποστηρίζει πίνακα JSON ή NDJSON", + "docsCount": "{count, plural, one {# έγγρ.} other {# έγγρ.}}", + "previewLabel": "Προεπισκόπηση (πρώτα 3 έγγραφα)", + "moreDocs": "… και {count} ακόμη έγγραφα", + "cancel": "Άκυρο", + "importing": "Εισαγωγή…", + "importCount": "{count, plural, one {Εισαγωγή # εγγρ.} other {Εισαγωγή # εγγρ.}}", + "importBtn": "Εισαγωγή" + }, + "schemaView": { + "analyzing": "Ανάλυση σχήματος…", + "loadFailed": "Αποτυχία ανάλυσης σχήματος", + "retry": "Επανάληψη", + "noDocs": "Δεν υπάρχουν έγγραφα για ανάλυση", + "sampled": "Δείγμα {docs} εγγράφων · {fields} πεδία", + "colField": "Πεδίο", + "colTypes": "Τύποι", + "colCoverage": "Κάλυψη" + }, + "indexManager": { + "loading": "Φόρτωση ευρετηρίων...", + "retry": "Επανάληψη", + "countLabel": "{count, plural, one {# ευρετήριο} other {# ευρετήρια}}", + "totalSize": "{size} συνολικά", + "statsDocs": "{count, plural, one {# έγγρ.} other {# έγγρ.}}", + "statsStorage": "{size} αποθήκευση", + "statsAvgObj": "{size} μ.ό./έγγρ.", + "newIndex": "Νέο ευρετήριο", + "createTitle": "Δημιουργία ευρετηρίου", + "fieldPlaceholder": "Όνομα πεδίου", + "ascending": "Αύξουσα", + "descending": "Φθίνουσα", + "addField": "Προσθήκη πεδίου", + "unique": "Μοναδικό", + "sparse": "Sparse", + "ttlLabel": "TTL (δευτερόλεπτα)", + "ttlPlaceholder": "π.χ. 3600", + "cancel": "Ακύρωση", + "create": "Δημιουργία", + "creating": "Δημιουργία...", + "created": "Το ευρετήριο δημιουργήθηκε", + "createFailed": "Αποτυχία δημιουργίας ευρετηρίου", + "fieldRequired": "Το όνομα πεδίου είναι υποχρεωτικό", + "dropped": "Το ευρετήριο \"{name}\" καταργήθηκε", + "dropFailed": "Αποτυχία κατάργησης ευρετηρίου", + "dropTitle": "Κατάργηση ευρετηρίου;", + "dropDescription": "Αυτό θα καταργήσει οριστικά το ευρετήριο \"{name}\". Τα ερωτήματα που το χρησιμοποιούν θα γίνουν πιο αργά.", + "dropping": "Κατάργηση...", + "dropConfirm": "Κατάργηση ευρετηρίου", + "badgeSystem": "σύστημα", + "badgeUnique": "μοναδικό", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Δεν βρέθηκαν ευρετήρια" + }, + "importDialog": { + "title": "Εισαγωγή εγγράφων στη συλλογή {collection}", + "onlyJson": "Υποστηρίζονται μόνο αρχεία .json", + "invalidStructure": "Το αρχείο πρέπει να περιέχει πίνακα ή αντικείμενο JSON", + "invalidJson": "Μη έγκυρο JSON: δεν ήταν δυνατή η ανάλυση του αρχείου", + "imported": "{count, plural, one {Εισήχθη # έγγραφο} other {Εισήχθησαν # έγγραφα}}", + "importFailed": "Η εισαγωγή απέτυχε", + "dropHint": "Σύρετε και αποθέστε ή κάντε κλικ για μεταφόρτωση", + "dropSubHint": "Υποστηρίζει πίνακα JSON ή NDJSON", + "docsCount": "{count, plural, one {# έγγρ.} other {# έγγρ.}}", + "previewLabel": "Προεπισκόπηση (πρώτα 3 έγγραφα)", + "moreDocs": "... και άλλα {count} έγγραφα", + "cancel": "Ακύρωση", + "importing": "Εισαγωγή...", + "importCount": "{count, plural, one {Εισαγωγή # εγγρ.} other {Εισαγωγή # εγγρ.}}", + "importBtn": "Εισαγωγή" + }, + "schemaView": { + "analyzing": "Ανάλυση σχήματος...", + "loadFailed": "Αποτυχία ανάλυσης σχήματος", + "retry": "Επανάληψη", + "noDocs": "Δεν υπάρχουν έγγραφα για ανάλυση", + "sampled": "Δείγμα {docs} εγγράφων · {fields} πεδία", + "colField": "Πεδίο", + "colTypes": "Τύποι", + "colCoverage": "Κάλυψη" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "Το cURL επικολλήθηκε και αναλύθηκε με επιτυχία", "responseCopied": "Η απάντηση αντιγράφηκε στο πρόχειρο", "codeCopied": "Ο κώδικας αντιγράφηκε στο πρόχειρο", - "copyFailed": "Αποτυχία αντιγραφής στο πρόχειρο" + "copyFailed": "Αποτυχία αντιγραφής στο πρόχειρο", + "curlCopied": "Η εντολή cURL αντιγράφηκε" }, "layout": { "collections": "Συλλογές", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Αποστολή...", "send": "Στέλνω", - "invalidJsonBodyHelp": "Αδύνατη η αποστολή: το σώμα JSON δεν είναι έγκυρο" + "invalidJsonBodyHelp": "Αδύνατη η αποστολή: το σώμα JSON δεν είναι έγκυρο", + "copyCurl": "Αντιγραφή ως cURL" }, "requestTabs": { "params": "Params", @@ -1915,7 +2087,10 @@ "placeholderName": "Το αίτημά μου", "labelFolder": "Ντοσιέ", "placeholderFolder": "Επιλέξτε ένα φάκελο", - "save": "Εκτός" + "save": "Εκτός", + "newFolder": "Νέος φάκελος", + "newFolderPlaceholder": "Όνομα φακέλου", + "create": "Δημιουργία" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Εκτέλεση ερωτήματος…", "emptyTitle": "Εκτελέστε ένα ερώτημα για να δείτε αποτελέσματα", "emptyHint": "Πατήστε ⌘↩ ή κάντε κλικ στο Εκτέλεση", - "toastNoConnection": "Δεν υπάρχει ενεργή σύνδεση." + "toastNoConnection": "Δεν υπάρχει ενεργή σύνδεση.", + "btnHistory": "Ιστορικό", + "btnSaveQuery": "Αποθήκευση ερωτήματος", + "savedSection": "Αποθηκευμένα", + "recentSection": "Πρόσφατα", + "emptyHistory": "Τίποτα ακόμη — εκτελέστε ή αποθηκεύστε ένα ερώτημα", + "savePlaceholder": "Όνομα ερωτήματος", + "deleteSaved": "Διαγραφή", + "toastQuerySaved": "Το ερώτημα αποθηκεύτηκε" }, "results": { "filterPlaceholder": "Φιλτράρισμα αποτελεσμάτων…", + "editHint": "Κάντε διπλό κλικ σε ένα κελί για επεξεργασία", + "toastRowUpdated": "Η γραμμή ενημερώθηκε", + "toastRowsUpdated": "{count, plural, one {# γραμμή ενημερώθηκε} other {# γραμμές ενημερώθηκαν}}", "rowCount": "{count} γραμμή(ές)", "rowCountFiltered": "{filtered} / {total} γραμμές", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/en.json b/apps/desktop-ui/messages/en.json index c58b276b..e94e917f 100644 --- a/apps/desktop-ui/messages/en.json +++ b/apps/desktop-ui/messages/en.json @@ -1604,7 +1604,10 @@ "cancel": "Cancel", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Failed to delete # collection} other {Failed to delete # collections}}", + "bulkDeleted": "{count, plural, one {Deleted # collection} other {Deleted # collections}}", + "bulkDeleteButton": "Delete {count, plural, one {# Collection} other {# Collections}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1681,7 +1684,25 @@ "explainReturned": "{n} returned", "explainDocsExamined": "{n} examined", "explainCollscanHint": "This query scans every document in the collection. Consider creating an index on the filtered fields.", - "explainRawLabel": "Explain output" + "explainRawLabel": "Explain output", + "docInsertFailed": "Failed to insert document", + "docUpdateFailed": "Failed to update document", + "indexesLoadFail": "Failed to load indexes", + "bulkDeleted": "{count, plural, one {# document deleted} other {# documents deleted}}", + "bulkDeleteFailed": "Bulk delete failed", + "selectedCount": "{count, plural, one {# document selected} other {# documents selected}}", + "deleteSelected": "Delete Selected", + "clearSelection": "Clear", + "statusLoading": "Loading...", + "statusShowing": "Showing {from}–{to} of {total} documents", + "statusEmpty": "0 documents", + "statusSelected": "{count} selected", + "bulkDeleteTitle": "Delete {count, plural, one {# document} other {# documents}}?", + "bulkDeleteDescription": "This will permanently delete {count, plural, one {# document} other {# documents}} from {collection}. This cannot be undone.", + "bulkDeleting": "Deleting...", + "bulkDeleteConfirm": "Delete All", + "queryErrorTitle": "Failed to load documents", + "retry": "Retry" }, "tabs": { "filterActive": "Active filter applied", @@ -1757,7 +1778,8 @@ "cancel": "Cancel", "export": "Export", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exports the current page only ({count, plural, one {# document} other {# documents}})" }, "jsonTree": { "typeLabel": "Type: {type}", @@ -1772,6 +1794,69 @@ "previewResult": "Preview results ({count})", "previewEmpty": "No documents at this stage", "previewFail": "Preview failed" + }, + "indexManager": { + "loading": "Loading indexes...", + "retry": "Retry", + "countLabel": "{count, plural, one {# index} other {# indexes}}", + "totalSize": "{size} total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} storage", + "statsAvgObj": "{size} avg/doc", + "newIndex": "New Index", + "createTitle": "Create Index", + "fieldPlaceholder": "Field name", + "ascending": "Ascending", + "descending": "Descending", + "addField": "Add field", + "unique": "Unique", + "sparse": "Sparse", + "ttlLabel": "TTL (seconds)", + "ttlPlaceholder": "e.g. 3600", + "cancel": "Cancel", + "create": "Create", + "creating": "Creating...", + "created": "Index created", + "createFailed": "Failed to create index", + "fieldRequired": "Field name is required", + "dropped": "Index \"{name}\" dropped", + "dropFailed": "Failed to drop index", + "dropTitle": "Drop index?", + "dropDescription": "This will permanently drop index \"{name}\". Queries using this index will slow down.", + "dropping": "Dropping...", + "dropConfirm": "Drop Index", + "badgeSystem": "system", + "badgeUnique": "unique", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "No indexes found" + }, + "importDialog": { + "title": "Import Documents into {collection}", + "onlyJson": "Only .json files are supported", + "invalidStructure": "File must contain a JSON array or object", + "invalidJson": "Invalid JSON: could not parse file", + "imported": "{count, plural, one {Imported # document} other {Imported # documents}}", + "importFailed": "Import failed", + "dropHint": "Drag & drop or click to upload", + "dropSubHint": "Supports JSON array or NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Preview (first 3 documents)", + "moreDocs": "... and {count} more documents", + "cancel": "Cancel", + "importing": "Importing...", + "importCount": "{count, plural, one {Import # Doc} other {Import # Docs}}", + "importBtn": "Import" + }, + "schemaView": { + "analyzing": "Analyzing schema...", + "loadFailed": "Failed to analyze schema", + "retry": "Retry", + "noDocs": "No documents to analyze", + "sampled": "Sampled {docs} documents · {fields} fields", + "colField": "Field", + "colTypes": "Types", + "colCoverage": "Coverage" } }, "ApiClient": { @@ -1789,7 +1874,8 @@ "curlNoUrl": "No URL found in that cURL command", "responseCopied": "Response copied to clipboard", "codeCopied": "Code copied to clipboard", - "copyFailed": "Failed to copy to clipboard" + "copyFailed": "Failed to copy to clipboard", + "curlCopied": "cURL command copied" }, "layout": { "collections": "Collections", @@ -1803,7 +1889,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sending...", "send": "Send", - "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid" + "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid", + "copyCurl": "Copy as cURL" }, "requestTabs": { "params": "Params", @@ -1940,7 +2027,10 @@ "placeholderName": "My request", "labelFolder": "Folder", "placeholderFolder": "Select a folder", - "save": "Save" + "save": "Save", + "newFolder": "New folder", + "newFolderPlaceholder": "Folder name", + "create": "Create" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3444,10 +3534,21 @@ "executing": "Executing query…", "emptyTitle": "Run a query to see results", "emptyHint": "Press ⌘↩ or click Run", - "toastNoConnection": "No active connection." + "toastNoConnection": "No active connection.", + "btnHistory": "History", + "btnSaveQuery": "Save query", + "savedSection": "Saved", + "recentSection": "Recent", + "emptyHistory": "Nothing here yet — run or save a query", + "savePlaceholder": "Query name", + "deleteSaved": "Delete", + "toastQuerySaved": "Query saved" }, "results": { "filterPlaceholder": "Filter results…", + "editHint": "Double-click a cell to edit", + "toastRowUpdated": "Row updated", + "toastRowsUpdated": "{count, plural, one {# row updated} other {# rows updated}}", "rowCount": "{count} {count, plural, one {row} other {rows}}", "rowCountFiltered": "{filtered} / {total} rows", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/es.json b/apps/desktop-ui/messages/es.json index 88e47a4b..a93bd0c0 100644 --- a/apps/desktop-ui/messages/es.json +++ b/apps/desktop-ui/messages/es.json @@ -1580,7 +1580,10 @@ "cancel": "Cancelar", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {No se pudo eliminar # colección} other {No se pudieron eliminar # colecciones}}", + "bulkDeleted": "{count, plural, one {# colección eliminada} other {# colecciones eliminadas}}", + "bulkDeleteButton": "Eliminar {count, plural, one {# colección} other {# colecciones}}" }, "document": { "docsBreadcrumb": "{n} documentos", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} devueltos", "explainDocsExamined": "{n} examinados", "explainCollscanHint": "Esta consulta escanea todos los documentos de la colección. Considera crear un índice sobre los campos filtrados.", - "explainRawLabel": "Salida de explain" + "explainRawLabel": "Salida de explain", + "docInsertFailed": "No se pudo insertar el documento", + "docUpdateFailed": "No se pudo actualizar el documento", + "indexesLoadFail": "No se pudieron cargar los índices", + "bulkDeleted": "{count, plural, one {# documento eliminado} other {# documentos eliminados}}", + "bulkDeleteFailed": "Falló la eliminación masiva", + "selectedCount": "{count, plural, one {# documento seleccionado} other {# documentos seleccionados}}", + "deleteSelected": "Eliminar seleccionados", + "clearSelection": "Limpiar", + "statusLoading": "Cargando...", + "statusShowing": "Mostrando {from}–{to} de {total} documentos", + "statusEmpty": "0 documentos", + "statusSelected": "{count} seleccionados", + "bulkDeleteTitle": "¿Eliminar {count, plural, one {# documento} other {# documentos}}?", + "bulkDeleteDescription": "Esto eliminará permanentemente {count, plural, one {# documento} other {# documentos}} de {collection}. Esta acción no se puede deshacer.", + "bulkDeleting": "Eliminando...", + "bulkDeleteConfirm": "Eliminar todo", + "queryErrorTitle": "No se pudieron cargar los documentos", + "retry": "Reintentar" }, "tabs": { "filterActive": "Filtro activo", @@ -1733,7 +1754,8 @@ "cancel": "Cancelar", "export": "Exportar", "sheetName": "Datos", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporta solo la página actual ({count, plural, one {# documento} other {# documentos}})" }, "jsonTree": { "typeLabel": "Tipo: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Resultados de vista previa ({count})", "previewEmpty": "No hay documentos en esta etapa", "previewFail": "Falló la vista previa" + }, + "indexManager": { + "loading": "Cargando índices...", + "retry": "Reintentar", + "countLabel": "{count, plural, one {# índice} other {# índices}}", + "totalSize": "{size} en total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} almacenamiento", + "statsAvgObj": "{size} media/doc", + "newIndex": "Nuevo índice", + "createTitle": "Crear índice", + "fieldPlaceholder": "Nombre del campo", + "ascending": "Ascendente", + "descending": "Descendente", + "addField": "Añadir campo", + "unique": "Único", + "sparse": "Sparse", + "ttlLabel": "TTL (segundos)", + "ttlPlaceholder": "p. ej. 3600", + "cancel": "Cancelar", + "create": "Crear", + "creating": "Creando...", + "created": "Índice creado", + "createFailed": "No se pudo crear el índice", + "fieldRequired": "El nombre del campo es obligatorio", + "dropped": "Índice \"{name}\" eliminado", + "dropFailed": "No se pudo eliminar el índice", + "dropTitle": "¿Eliminar índice?", + "dropDescription": "Esto eliminará permanentemente el índice \"{name}\". Las consultas que usen este índice se ralentizarán.", + "dropping": "Eliminando...", + "dropConfirm": "Eliminar índice", + "badgeSystem": "sistema", + "badgeUnique": "único", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "No se encontraron índices" + }, + "importDialog": { + "title": "Importar documentos a {collection}", + "onlyJson": "Solo se admiten archivos .json", + "invalidStructure": "El archivo debe contener un array u objeto JSON", + "invalidJson": "JSON no válido: no se pudo analizar el archivo", + "imported": "{count, plural, one {# documento importado} other {# documentos importados}}", + "importFailed": "Falló la importación", + "dropHint": "Arrastra y suelta o haz clic para subir", + "dropSubHint": "Admite array JSON o NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Vista previa (primeros 3 documentos)", + "moreDocs": "... y {count} documentos más", + "cancel": "Cancelar", + "importing": "Importando...", + "importCount": "{count, plural, one {Importar # doc} other {Importar # docs}}", + "importBtn": "Importar" + }, + "schemaView": { + "analyzing": "Analizando esquema...", + "loadFailed": "No se pudo analizar el esquema", + "retry": "Reintentar", + "noDocs": "No hay documentos para analizar", + "sampled": "Muestra de {docs} documentos · {fields} campos", + "colField": "Campo", + "colTypes": "Tipos", + "colCoverage": "Cobertura" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL pegado y analizado correctamente", "responseCopied": "Respuesta copiada al portapapeles", "codeCopied": "Código copiado al portapapeles", - "copyFailed": "Error al copiar al portapapeles" + "copyFailed": "Error al copiar al portapapeles", + "curlCopied": "Comando cURL copiado" }, "layout": { "collections": "Colecciones", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.ejemplo.com/v1/...", "sending": "Enviando...", "send": "Enviar", - "invalidJsonBodyHelp": "No se puede enviar: el cuerpo JSON no es válido" + "invalidJsonBodyHelp": "No se puede enviar: el cuerpo JSON no es válido", + "copyCurl": "Copiar como cURL" }, "requestTabs": { "params": "Parámetros", @@ -1915,7 +2002,10 @@ "placeholderName": "Mi solicitud", "labelFolder": "Carpeta", "placeholderFolder": "Selecciona una carpeta", - "save": "Guardar" + "save": "Guardar", + "newFolder": "Nueva carpeta", + "newFolderPlaceholder": "Nombre de la carpeta", + "create": "Crear" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Ejecutando consulta…", "emptyTitle": "Ejecuta una consulta para ver los resultados", "emptyHint": "Pulsa ⌘↩ o haz clic en Ejecutar", - "toastNoConnection": "No hay conexión activa." + "toastNoConnection": "No hay conexión activa.", + "btnHistory": "Historial", + "btnSaveQuery": "Guardar consulta", + "savedSection": "Guardadas", + "recentSection": "Recientes", + "emptyHistory": "Nada aún — ejecuta o guarda una consulta", + "savePlaceholder": "Nombre de la consulta", + "deleteSaved": "Eliminar", + "toastQuerySaved": "Consulta guardada" }, "results": { "filterPlaceholder": "Filtrar resultados…", + "editHint": "Haz doble clic en una celda para editar", + "toastRowUpdated": "Fila actualizada", + "toastRowsUpdated": "{count, plural, one {# fila actualizada} other {# filas actualizadas}}", "rowCount": "{count} {count, plural, one {fila} other {filas}}", "rowCountFiltered": "{filtered} / {total} filas", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/fa.json b/apps/desktop-ui/messages/fa.json index decba415..414e3619 100644 --- a/apps/desktop-ui/messages/fa.json +++ b/apps/desktop-ui/messages/fa.json @@ -1580,7 +1580,10 @@ "cancel": "لغو کنید", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {حذف # مجموعه ناموفق بود} other {حذف # مجموعه ناموفق بود}}", + "bulkDeleted": "{count, plural, one {# مجموعه حذف شد} other {# مجموعه حذف شد}}", + "bulkDeleteButton": "حذف {count, plural, one {# مجموعه} other {# مجموعه}}" }, "document": { "docsBreadcrumb": "{n} اسناد", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} بازگردانده شد", "explainDocsExamined": "{n} بررسی شد", "explainCollscanHint": "این کوئری همه اسناد کلکسیون را اسکن می‌کند. ساخت ایندکس روی فیلدهای فیلترشده را در نظر بگیرید.", - "explainRawLabel": "خروجی explain" + "explainRawLabel": "خروجی explain", + "docInsertFailed": "درج سند ناموفق بود", + "docUpdateFailed": "به‌روزرسانی سند ناموفق بود", + "indexesLoadFail": "بارگذاری ایندکس‌ها ناموفق بود", + "bulkDeleted": "{count, plural, one {# سند حذف شد} other {# سند حذف شد}}", + "bulkDeleteFailed": "حذف گروهی ناموفق بود", + "selectedCount": "{count, plural, one {# سند انتخاب شده} other {# سند انتخاب شده}}", + "deleteSelected": "حذف موارد انتخاب‌شده", + "clearSelection": "پاک کردن", + "statusLoading": "در حال بارگذاری...", + "statusShowing": "نمایش {from}–{to} از {total} سند", + "statusEmpty": "0 سند", + "statusSelected": "{count} انتخاب شده", + "bulkDeleteTitle": "{count, plural, one {# سند} other {# سند}} حذف شود؟", + "bulkDeleteDescription": "این کار {count, plural, one {# سند} other {# سند}} را برای همیشه از {collection} حذف می‌کند. این عمل قابل بازگشت نیست.", + "bulkDeleting": "در حال حذف...", + "bulkDeleteConfirm": "حذف همه", + "queryErrorTitle": "بارگذاری اسناد ناموفق بود", + "retry": "تلاش دوباره" }, "tabs": { "filterActive": "فیلتر فعال اعمال شد", @@ -1733,7 +1754,8 @@ "cancel": "لغو کنید", "export": "صادرات", "sheetName": "داده ها", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "فقط صفحه فعلی را صادر می‌کند ({count, plural, one {# سند} other {# سند}})" }, "jsonTree": { "typeLabel": "نوع: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "نتایج پیش‌نمایش ({count})", "previewEmpty": "سندی در این مرحله وجود ندارد", "previewFail": "پیش‌نمایش ناموفق بود" + }, + "indexManager": { + "loading": "در حال بارگذاری ایندکس‌ها...", + "retry": "تلاش دوباره", + "countLabel": "{count, plural, one {# ایندکس} other {# ایندکس}}", + "totalSize": "{size} در مجموع", + "statsDocs": "{count, plural, one {# سند} other {# سند}}", + "statsStorage": "{size} فضای ذخیره‌سازی", + "statsAvgObj": "{size} میانگین/سند", + "newIndex": "ایندکس جدید", + "createTitle": "ایجاد ایندکس", + "fieldPlaceholder": "نام فیلد", + "ascending": "صعودی", + "descending": "نزولی", + "addField": "افزودن فیلد", + "unique": "یکتا", + "sparse": "Sparse", + "ttlLabel": "TTL (ثانیه)", + "ttlPlaceholder": "مثلاً 3600", + "cancel": "لغو", + "create": "ایجاد", + "creating": "در حال ایجاد...", + "created": "ایندکس ایجاد شد", + "createFailed": "ایجاد ایندکس ناموفق بود", + "fieldRequired": "نام فیلد الزامی است", + "dropped": "ایندکس \"{name}\" حذف شد", + "dropFailed": "حذف ایندکس ناموفق بود", + "dropTitle": "ایندکس حذف شود؟", + "dropDescription": "این کار ایندکس \"{name}\" را برای همیشه حذف می‌کند. کوئری‌هایی که از این ایندکس استفاده می‌کنند کند خواهند شد.", + "dropping": "در حال حذف...", + "dropConfirm": "حذف ایندکس", + "badgeSystem": "سیستمی", + "badgeUnique": "یکتا", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "ایندکسی یافت نشد" + }, + "importDialog": { + "title": "وارد کردن اسناد به {collection}", + "onlyJson": "فقط فایل‌های .json پشتیبانی می‌شوند", + "invalidStructure": "فایل باید شامل یک آرایه یا شیء JSON باشد", + "invalidJson": "JSON نامعتبر: تجزیه فایل ممکن نبود", + "imported": "{count, plural, one {# سند وارد شد} other {# سند وارد شد}}", + "importFailed": "وارد کردن ناموفق بود", + "dropHint": "بکشید و رها کنید یا برای بارگذاری کلیک کنید", + "dropSubHint": "از آرایه JSON یا NDJSON پشتیبانی می‌کند", + "docsCount": "{count, plural, one {# سند} other {# سند}}", + "previewLabel": "پیش‌نمایش (3 سند اول)", + "moreDocs": "... و {count} سند دیگر", + "cancel": "لغو", + "importing": "در حال وارد کردن...", + "importCount": "{count, plural, one {وارد کردن # سند} other {وارد کردن # سند}}", + "importBtn": "وارد کردن" + }, + "schemaView": { + "analyzing": "در حال تحلیل اسکیما...", + "loadFailed": "تحلیل اسکیما ناموفق بود", + "retry": "تلاش دوباره", + "noDocs": "سندی برای تحلیل وجود ندارد", + "sampled": "نمونه‌گیری از {docs} سند · {fields} فیلد", + "colField": "فیلد", + "colTypes": "انواع", + "colCoverage": "پوشش" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL با موفقیت جای‌گذاری و تجزیه شد", "responseCopied": "پاسخ در کلیپ بورد کپی شد", "codeCopied": "کد در کلیپ بورد کپی شد", - "copyFailed": "کپی در کلیپ‌بورد ناموفق بود" + "copyFailed": "کپی در کلیپ‌بورد ناموفق بود", + "curlCopied": "دستور cURL کپی شد" }, "layout": { "collections": "مجموعه ها", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "ارسال...", "send": "ارسال کنید", - "invalidJsonBodyHelp": "ارسال امکان‌پذیر نیست: بدنه JSON نامعتبر است" + "invalidJsonBodyHelp": "ارسال امکان‌پذیر نیست: بدنه JSON نامعتبر است", + "copyCurl": "کپی به‌صورت cURL" }, "requestTabs": { "params": "پارامترها", @@ -1915,7 +2002,10 @@ "placeholderName": "درخواست من", "labelFolder": "پوشه", "placeholderFolder": "یک پوشه را انتخاب کنید", - "save": "ذخیره کنید" + "save": "ذخیره کنید", + "newFolder": "پوشه جدید", + "newFolderPlaceholder": "نام پوشه", + "create": "ایجاد" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "در حال اجرای پرس‌وجو…", "emptyTitle": "یک پرس‌وجو اجرا کنید تا نتایج را ببینید", "emptyHint": "⌘↩ را فشار دهید یا روی اجرا کلیک کنید", - "toastNoConnection": "هیچ اتصال فعالی وجود ندارد." + "toastNoConnection": "هیچ اتصال فعالی وجود ندارد.", + "btnHistory": "تاریخچه", + "btnSaveQuery": "ذخیره کوئری", + "savedSection": "ذخیره‌شده", + "recentSection": "اخیر", + "emptyHistory": "هنوز چیزی نیست — کوئری اجرا یا ذخیره کنید", + "savePlaceholder": "نام کوئری", + "deleteSaved": "حذف", + "toastQuerySaved": "کوئری ذخیره شد" }, "results": { "filterPlaceholder": "فیلتر نتایج…", + "editHint": "برای ویرایش روی سلول دوبار کلیک کنید", + "toastRowUpdated": "ردیف به‌روزرسانی شد", + "toastRowsUpdated": "{count, plural, one {# ردیف به‌روزرسانی شد} other {# ردیف به‌روزرسانی شدند}}", "rowCount": "{count} ردیف", "rowCountFiltered": "{filtered} / {total} ردیف", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/fr.json b/apps/desktop-ui/messages/fr.json index 012bba6f..b1035452 100644 --- a/apps/desktop-ui/messages/fr.json +++ b/apps/desktop-ui/messages/fr.json @@ -1580,7 +1580,10 @@ "cancel": "Annuler", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Échec de la suppression de # collection} other {Échec de la suppression de # collections}}", + "bulkDeleted": "{count, plural, one {# collection supprimée} other {# collections supprimées}}", + "bulkDeleteButton": "Supprimer {count, plural, one {# collection} other {# collections}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} renvoyés", "explainDocsExamined": "{n} examinés", "explainCollscanHint": "Cette requête parcourt chaque document de la collection. Envisagez de créer un index sur les champs filtrés.", - "explainRawLabel": "Sortie explain" + "explainRawLabel": "Sortie explain", + "docInsertFailed": "Échec de l'insertion du document", + "docUpdateFailed": "Échec de la mise à jour du document", + "indexesLoadFail": "Échec du chargement des index", + "bulkDeleted": "{count, plural, one {# document supprimé} other {# documents supprimés}}", + "bulkDeleteFailed": "Échec de la suppression en masse", + "selectedCount": "{count, plural, one {# document sélectionné} other {# documents sélectionnés}}", + "deleteSelected": "Supprimer la sélection", + "clearSelection": "Effacer", + "statusLoading": "Chargement...", + "statusShowing": "Affichage de {from}–{to} sur {total} documents", + "statusEmpty": "0 document", + "statusSelected": "{count} sélectionné(s)", + "bulkDeleteTitle": "Supprimer {count, plural, one {# document} other {# documents}} ?", + "bulkDeleteDescription": "Cela supprimera définitivement {count, plural, one {# document} other {# documents}} de {collection}. Cette action est irréversible.", + "bulkDeleting": "Suppression...", + "bulkDeleteConfirm": "Tout supprimer", + "queryErrorTitle": "Échec du chargement des documents", + "retry": "Réessayer" }, "tabs": { "filterActive": "Filtre actif", @@ -1733,7 +1754,8 @@ "cancel": "Annuler", "export": "Exporter", "sheetName": "Données", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporte uniquement la page actuelle ({count, plural, one {# document} other {# documents}})" }, "jsonTree": { "typeLabel": "Type : {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Résultats de l'aperçu ({count})", "previewEmpty": "Aucun document à cette étape", "previewFail": "Échec de l'aperçu" + }, + "indexManager": { + "loading": "Chargement des index...", + "retry": "Réessayer", + "countLabel": "{count, plural, one {# index} other {# index}}", + "totalSize": "{size} au total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} de stockage", + "statsAvgObj": "{size} moy./doc", + "newIndex": "Nouvel index", + "createTitle": "Créer un index", + "fieldPlaceholder": "Nom du champ", + "ascending": "Croissant", + "descending": "Décroissant", + "addField": "Ajouter un champ", + "unique": "Unique", + "sparse": "Sparse", + "ttlLabel": "TTL (secondes)", + "ttlPlaceholder": "ex. 3600", + "cancel": "Annuler", + "create": "Créer", + "creating": "Création...", + "created": "Index créé", + "createFailed": "Échec de la création de l'index", + "fieldRequired": "Le nom du champ est requis", + "dropped": "Index \"{name}\" supprimé", + "dropFailed": "Échec de la suppression de l'index", + "dropTitle": "Supprimer l'index ?", + "dropDescription": "Cela supprimera définitivement l'index \"{name}\". Les requêtes utilisant cet index seront ralenties.", + "dropping": "Suppression...", + "dropConfirm": "Supprimer l'index", + "badgeSystem": "système", + "badgeUnique": "unique", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Aucun index trouvé" + }, + "importDialog": { + "title": "Importer des documents dans {collection}", + "onlyJson": "Seuls les fichiers .json sont pris en charge", + "invalidStructure": "Le fichier doit contenir un tableau ou un objet JSON", + "invalidJson": "JSON invalide : impossible d'analyser le fichier", + "imported": "{count, plural, one {# document importé} other {# documents importés}}", + "importFailed": "Échec de l'importation", + "dropHint": "Glissez-déposez ou cliquez pour téléverser", + "dropSubHint": "Prend en charge les tableaux JSON et NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Aperçu (3 premiers documents)", + "moreDocs": "... et {count} documents de plus", + "cancel": "Annuler", + "importing": "Importation...", + "importCount": "{count, plural, one {Importer # doc} other {Importer # docs}}", + "importBtn": "Importer" + }, + "schemaView": { + "analyzing": "Analyse du schéma...", + "loadFailed": "Échec de l'analyse du schéma", + "retry": "Réessayer", + "noDocs": "Aucun document à analyser", + "sampled": "Échantillon de {docs} documents · {fields} champs", + "colField": "Champ", + "colTypes": "Types", + "colCoverage": "Couverture" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL collé et analysé", "responseCopied": "Réponse copiée", "codeCopied": "Code copié", - "copyFailed": "Impossible de copier dans le presse-papiers" + "copyFailed": "Impossible de copier dans le presse-papiers", + "curlCopied": "Commande cURL copiée" }, "layout": { "collections": "Collections", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Envoi…", "send": "Envoyer", - "invalidJsonBodyHelp": "Envoi impossible : le corps JSON est invalide" + "invalidJsonBodyHelp": "Envoi impossible : le corps JSON est invalide", + "copyCurl": "Copier en cURL" }, "requestTabs": { "params": "Paramètres", @@ -1915,7 +2002,10 @@ "placeholderName": "Ma requête", "labelFolder": "Dossier", "placeholderFolder": "Choisir un dossier", - "save": "Enregistrer" + "save": "Enregistrer", + "newFolder": "Nouveau dossier", + "newFolderPlaceholder": "Nom du dossier", + "create": "Créer" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Exécution en cours…", "emptyTitle": "Exécutez une requête pour voir les résultats", "emptyHint": "Appuyez sur ⌘↩ ou cliquez sur Exécuter", - "toastNoConnection": "Aucune connexion active." + "toastNoConnection": "Aucune connexion active.", + "btnHistory": "Historique", + "btnSaveQuery": "Enregistrer la requête", + "savedSection": "Enregistrées", + "recentSection": "Récentes", + "emptyHistory": "Rien pour l'instant — exécutez ou enregistrez une requête", + "savePlaceholder": "Nom de la requête", + "deleteSaved": "Supprimer", + "toastQuerySaved": "Requête enregistrée" }, "results": { "filterPlaceholder": "Filtrer les résultats…", + "editHint": "Double-cliquez sur une cellule pour modifier", + "toastRowUpdated": "Ligne mise à jour", + "toastRowsUpdated": "{count, plural, one {# ligne mise à jour} other {# lignes mises à jour}}", "rowCount": "{count} {count, plural, one {ligne} other {lignes}}", "rowCountFiltered": "{filtered} / {total} lignes", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/id.json b/apps/desktop-ui/messages/id.json index 0b2553a3..1f7c7675 100644 --- a/apps/desktop-ui/messages/id.json +++ b/apps/desktop-ui/messages/id.json @@ -1580,7 +1580,13 @@ "cancel": "Batalkan", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Gagal menghapus # koleksi} other {Gagal menghapus # koleksi}}", + "bulkDeleted": "{count, plural, one {# koleksi dihapus} other {# koleksi dihapus}}", + "bulkDeleteButton": "{count, plural, one {Hapus # Koleksi} other {Hapus # Koleksi}}", + "bulkDeleteFailed": "{count, plural, other {Gagal menghapus # koleksi}}", + "bulkDeleted": "{count, plural, other {Menghapus # koleksi}}", + "bulkDeleteButton": "Hapus {count, plural, other {# koleksi}}" }, "document": { "docsBreadcrumb": "{n} dokumen", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} dikembalikan", "explainDocsExamined": "{n} diperiksa", "explainCollscanHint": "Kueri ini memindai setiap dokumen dalam koleksi. Pertimbangkan membuat indeks pada bidang yang difilter.", - "explainRawLabel": "Keluaran explain" + "explainRawLabel": "Keluaran explain", + "docInsertFailed": "Gagal menyisipkan dokumen", + "docUpdateFailed": "Gagal memperbarui dokumen", + "indexesLoadFail": "Gagal memuat indeks", + "bulkDeleted": "{count, plural, one {# dokumen dihapus} other {# dokumen dihapus}}", + "bulkDeleteFailed": "Penghapusan massal gagal", + "selectedCount": "{count, plural, one {# dokumen dipilih} other {# dokumen dipilih}}", + "deleteSelected": "Hapus yang dipilih", + "clearSelection": "Bersihkan", + "statusLoading": "Memuat…", + "statusShowing": "Menampilkan {from}–{to} dari {total} dokumen", + "statusEmpty": "0 dokumen", + "statusSelected": "{count} dipilih", + "bulkDeleteTitle": "Hapus {count, plural, one {# dokumen} other {# dokumen}}?", + "bulkDeleteDescription": "Ini akan menghapus permanen {count, plural, one {# dokumen} other {# dokumen}} dari {collection}. Tindakan ini tidak dapat dibatalkan.", + "bulkDeleting": "Menghapus…", + "bulkDeleteConfirm": "Hapus semua", + "queryErrorTitle": "Gagal memuat dokumen", + "retry": "Coba lagi", + "docInsertFailed": "Gagal menyisipkan dokumen", + "docUpdateFailed": "Gagal memperbarui dokumen", + "indexesLoadFail": "Gagal memuat indeks", + "bulkDeleted": "{count, plural, other {# dokumen dihapus}}", + "bulkDeleteFailed": "Penghapusan massal gagal", + "selectedCount": "{count, plural, other {# dokumen dipilih}}", + "deleteSelected": "Hapus yang dipilih", + "clearSelection": "Bersihkan", + "statusLoading": "Memuat...", + "statusShowing": "Menampilkan {from}-{to} dari {total} dokumen", + "statusEmpty": "0 dokumen", + "statusSelected": "{count} dipilih", + "bulkDeleteTitle": "Hapus {count, plural, other {# dokumen}}?", + "bulkDeleteDescription": "Ini akan menghapus permanen {count, plural, other {# dokumen}} dari {collection}. Tindakan ini tidak dapat dibatalkan.", + "bulkDeleting": "Menghapus...", + "bulkDeleteConfirm": "Hapus semua", + "queryErrorTitle": "Gagal memuat dokumen", + "retry": "Coba lagi" }, "tabs": { "filterActive": "Filter aktif diterapkan", @@ -1733,7 +1775,9 @@ "cancel": "Batalkan", "export": "Ekspor", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Hanya mengekspor halaman saat ini ({count, plural, one {# dokumen} other {# dokumen}})", + "pageOnlyNote": "Hanya mengekspor halaman saat ini ({count, plural, other {# dokumen}})" }, "jsonTree": { "typeLabel": "Tipe: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Hasil pratinjau ({count})", "previewEmpty": "Tidak ada dokumen pada tahap ini", "previewFail": "Pratinjau gagal" + }, + "indexManager": { + "loading": "Memuat indeks…", + "retry": "Coba lagi", + "countLabel": "{count, plural, one {# indeks} other {# indeks}}", + "totalSize": "total {size}", + "statsDocs": "{count, plural, one {# dok} other {# dok}}", + "statsStorage": "penyimpanan {size}", + "statsAvgObj": "{size} rata-rata/dok", + "newIndex": "Indeks Baru", + "createTitle": "Buat Indeks", + "fieldPlaceholder": "Nama bidang", + "ascending": "Menaik", + "descending": "Menurun", + "addField": "Tambah bidang", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (detik)", + "ttlPlaceholder": "mis. 3600", + "cancel": "Batal", + "create": "Buat", + "creating": "Membuat…", + "created": "Indeks dibuat", + "createFailed": "Gagal membuat indeks", + "fieldRequired": "Nama bidang wajib diisi", + "dropped": "Indeks \"{name}\" dihapus", + "dropFailed": "Gagal menghapus indeks", + "dropTitle": "Hapus indeks?", + "dropDescription": "Ini akan menghapus permanen indeks \"{name}\". Kueri yang menggunakan indeks ini akan melambat.", + "dropping": "Menghapus…", + "dropConfirm": "Hapus Indeks", + "badgeSystem": "sistem", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Tidak ada indeks ditemukan" + }, + "importDialog": { + "title": "Impor Dokumen ke {collection}", + "onlyJson": "Hanya file .json yang didukung", + "invalidStructure": "File harus berisi array atau objek JSON", + "invalidJson": "JSON tidak valid: tidak dapat mengurai file", + "imported": "{count, plural, one {# dokumen diimpor} other {# dokumen diimpor}}", + "importFailed": "Impor gagal", + "dropHint": "Seret & lepas atau klik untuk mengunggah", + "dropSubHint": "Mendukung array JSON atau NDJSON", + "docsCount": "{count, plural, one {# dok} other {# dok}}", + "previewLabel": "Pratinjau (3 dokumen pertama)", + "moreDocs": "… dan {count} dokumen lainnya", + "cancel": "Batal", + "importing": "Mengimpor…", + "importCount": "{count, plural, one {Impor # Dok} other {Impor # Dok}}", + "importBtn": "Impor" + }, + "schemaView": { + "analyzing": "Menganalisis skema…", + "loadFailed": "Gagal menganalisis skema", + "retry": "Coba lagi", + "noDocs": "Tidak ada dokumen untuk dianalisis", + "sampled": "Sampel {docs} dokumen · {fields} bidang", + "colField": "Bidang", + "colTypes": "Tipe", + "colCoverage": "Cakupan" + }, + "indexManager": { + "loading": "Memuat indeks...", + "retry": "Coba lagi", + "countLabel": "{count, plural, other {# indeks}}", + "totalSize": "{size} total", + "statsDocs": "{count, plural, other {# dok}}", + "statsStorage": "{size} penyimpanan", + "statsAvgObj": "{size} rata-rata/dok", + "newIndex": "Indeks baru", + "createTitle": "Buat indeks", + "fieldPlaceholder": "Nama bidang", + "ascending": "Menaik", + "descending": "Menurun", + "addField": "Tambah bidang", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (detik)", + "ttlPlaceholder": "mis. 3600", + "cancel": "Batal", + "create": "Buat", + "creating": "Membuat...", + "created": "Indeks dibuat", + "createFailed": "Gagal membuat indeks", + "fieldRequired": "Nama bidang wajib diisi", + "dropped": "Indeks \"{name}\" dihapus", + "dropFailed": "Gagal menghapus indeks", + "dropTitle": "Hapus indeks?", + "dropDescription": "Ini akan menghapus permanen indeks \"{name}\". Kueri yang menggunakan indeks ini akan lebih lambat.", + "dropping": "Menghapus...", + "dropConfirm": "Hapus indeks", + "badgeSystem": "sistem", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Tidak ada indeks ditemukan" + }, + "importDialog": { + "title": "Impor dokumen ke {collection}", + "onlyJson": "Hanya file .json yang didukung", + "invalidStructure": "File harus berisi array atau objek JSON", + "invalidJson": "JSON tidak valid: tidak dapat mengurai file", + "imported": "{count, plural, other {# dokumen diimpor}}", + "importFailed": "Impor gagal", + "dropHint": "Seret & lepas atau klik untuk mengunggah", + "dropSubHint": "Mendukung array JSON atau NDJSON", + "docsCount": "{count, plural, other {# dok}}", + "previewLabel": "Pratinjau (3 dokumen pertama)", + "moreDocs": "... dan {count} dokumen lainnya", + "cancel": "Batal", + "importing": "Mengimpor...", + "importCount": "{count, plural, other {Impor # dok}}", + "importBtn": "Impor" + }, + "schemaView": { + "analyzing": "Menganalisis skema...", + "loadFailed": "Gagal menganalisis skema", + "retry": "Coba lagi", + "noDocs": "Tidak ada dokumen untuk dianalisis", + "sampled": "Mengambil sampel {docs} dokumen · {fields} bidang", + "colField": "Bidang", + "colTypes": "Tipe", + "colCoverage": "Cakupan" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL berhasil ditempelkan dan diuraikan", "responseCopied": "Tanggapan disalin ke clipboard", "codeCopied": "Disalin ke papan klip", - "copyFailed": "Gagal menyalin ke papan klip" + "copyFailed": "Gagal menyalin ke papan klip", + "curlCopied": "Perintah cURL disalin" }, "layout": { "collections": "Penagihan", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Mengirim...", "send": "Kirim", - "invalidJsonBodyHelp": "Tidak dapat mengirim: isi JSON tidak valid" + "invalidJsonBodyHelp": "Tidak dapat mengirim: isi JSON tidak valid", + "copyCurl": "Salin sebagai cURL" }, "requestTabs": { "params": "parameter", @@ -1915,7 +2087,10 @@ "placeholderName": "My request", "labelFolder": "Folder", "placeholderFolder": "Select a folder", - "save": "Save" + "save": "Save", + "newFolder": "Folder baru", + "newFolderPlaceholder": "Nama folder", + "create": "Buat" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Menjalankan kueri…", "emptyTitle": "Jalankan kueri untuk melihat hasil", "emptyHint": "Tekan ⌘↩ atau klik Jalankan", - "toastNoConnection": "Tidak ada koneksi aktif." + "toastNoConnection": "Tidak ada koneksi aktif.", + "btnHistory": "Riwayat", + "btnSaveQuery": "Simpan kueri", + "savedSection": "Tersimpan", + "recentSection": "Terbaru", + "emptyHistory": "Belum ada apa-apa — jalankan atau simpan kueri", + "savePlaceholder": "Nama kueri", + "deleteSaved": "Hapus", + "toastQuerySaved": "Kueri disimpan" }, "results": { "filterPlaceholder": "Filter hasil…", + "editHint": "Klik dua kali sel untuk mengedit", + "toastRowUpdated": "Baris diperbarui", + "toastRowsUpdated": "{count, plural, other {# baris diperbarui}}", "rowCount": "{count} baris", "rowCountFiltered": "{filtered} / {total} baris", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/it.json b/apps/desktop-ui/messages/it.json index 431ddfac..cc6e30a0 100644 --- a/apps/desktop-ui/messages/it.json +++ b/apps/desktop-ui/messages/it.json @@ -1580,7 +1580,10 @@ "cancel": "Cancellare", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Impossibile eliminare # collezione} other {Impossibile eliminare # collezioni}}", + "bulkDeleted": "{count, plural, one {# collezione eliminata} other {# collezioni eliminate}}", + "bulkDeleteButton": "Elimina {count, plural, one {# collezione} other {# collezioni}}" }, "document": { "docsBreadcrumb": "{n} documenti", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} restituiti", "explainDocsExamined": "{n} esaminati", "explainCollscanHint": "Questa query scansiona ogni documento della collection. Valuta la creazione di un indice sui campi filtrati.", - "explainRawLabel": "Output explain" + "explainRawLabel": "Output explain", + "docInsertFailed": "Impossibile inserire il documento", + "docUpdateFailed": "Impossibile aggiornare il documento", + "indexesLoadFail": "Impossibile caricare gli indici", + "bulkDeleted": "{count, plural, one {# documento eliminato} other {# documenti eliminati}}", + "bulkDeleteFailed": "Eliminazione in blocco non riuscita", + "selectedCount": "{count, plural, one {# documento selezionato} other {# documenti selezionati}}", + "deleteSelected": "Elimina selezionati", + "clearSelection": "Deseleziona", + "statusLoading": "Caricamento...", + "statusShowing": "Visualizzazione di {from}–{to} su {total} documenti", + "statusEmpty": "0 documenti", + "statusSelected": "{count} selezionati", + "bulkDeleteTitle": "Eliminare {count, plural, one {# documento} other {# documenti}}?", + "bulkDeleteDescription": "Questo eliminerà definitivamente {count, plural, one {# documento} other {# documenti}} da {collection}. L'operazione non può essere annullata.", + "bulkDeleting": "Eliminazione...", + "bulkDeleteConfirm": "Elimina tutto", + "queryErrorTitle": "Impossibile caricare i documenti", + "retry": "Riprova" }, "tabs": { "filterActive": "Filtro attivo applicato", @@ -1729,15 +1750,16 @@ "formatCsv": "CSV", "selectFields": "Seleziona Campi", "selectAll": "Seleziona tutto", - "fieldsSelected": "{contare, plurale, uno {# field selected} altro {# fields selected}}", + "fieldsSelected": "{count, plural, one {# campo selezionato} other {# campi selezionati}}", "cancel": "Cancellare", "export": "Esportare", "sheetName": "Dati", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Esporta solo la pagina corrente ({count, plural, one {# documento} other {# documenti}})" }, "jsonTree": { "typeLabel": "Tipo: {type}", - "itemSummary": "{contare, plurale, uno {# item} altro {# items}}" + "itemSummary": "{count, plural, one {# elemento} other {# elementi}}" }, "pipeline": { "empty": "Nessuna fase ancora. Aggiungi una fase per costruire la pipeline.", @@ -1748,6 +1770,69 @@ "previewResult": "Risultati anteprima ({count})", "previewEmpty": "Nessun documento in questa fase", "previewFail": "Anteprima non riuscita" + }, + "indexManager": { + "loading": "Caricamento indici...", + "retry": "Riprova", + "countLabel": "{count, plural, one {# indice} other {# indici}}", + "totalSize": "{size} in totale", + "statsDocs": "{count, plural, one {# doc} other {# doc}}", + "statsStorage": "{size} di archiviazione", + "statsAvgObj": "{size} media/doc", + "newIndex": "Nuovo indice", + "createTitle": "Crea indice", + "fieldPlaceholder": "Nome del campo", + "ascending": "Crescente", + "descending": "Decrescente", + "addField": "Aggiungi campo", + "unique": "Univoco", + "sparse": "Sparse", + "ttlLabel": "TTL (secondi)", + "ttlPlaceholder": "es. 3600", + "cancel": "Annulla", + "create": "Crea", + "creating": "Creazione...", + "created": "Indice creato", + "createFailed": "Impossibile creare l'indice", + "fieldRequired": "Il nome del campo è obbligatorio", + "dropped": "Indice \"{name}\" eliminato", + "dropFailed": "Impossibile eliminare l'indice", + "dropTitle": "Eliminare l'indice?", + "dropDescription": "Questo eliminerà definitivamente l'indice \"{name}\". Le query che utilizzano questo indice diventeranno più lente.", + "dropping": "Eliminazione...", + "dropConfirm": "Elimina indice", + "badgeSystem": "sistema", + "badgeUnique": "univoco", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Nessun indice trovato" + }, + "importDialog": { + "title": "Importa documenti in {collection}", + "onlyJson": "Sono supportati solo file .json", + "invalidStructure": "Il file deve contenere un array o un oggetto JSON", + "invalidJson": "JSON non valido: impossibile analizzare il file", + "imported": "{count, plural, one {# documento importato} other {# documenti importati}}", + "importFailed": "Importazione non riuscita", + "dropHint": "Trascina qui o fai clic per caricare", + "dropSubHint": "Supporta array JSON o NDJSON", + "docsCount": "{count, plural, one {# doc} other {# doc}}", + "previewLabel": "Anteprima (primi 3 documenti)", + "moreDocs": "... e altri {count} documenti", + "cancel": "Annulla", + "importing": "Importazione...", + "importCount": "{count, plural, one {Importa # doc} other {Importa # doc}}", + "importBtn": "Importa" + }, + "schemaView": { + "analyzing": "Analisi dello schema...", + "loadFailed": "Impossibile analizzare lo schema", + "retry": "Riprova", + "noDocs": "Nessun documento da analizzare", + "sampled": "Campione di {docs} documenti · {fields} campi", + "colField": "Campo", + "colTypes": "Tipi", + "colCoverage": "Copertura" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL incollato e analizzato correttamente", "responseCopied": "Risposta copiata negli appunti", "codeCopied": "Codice copiato negli appunti", - "copyFailed": "Copia negli appunti non riuscita" + "copyFailed": "Copia negli appunti non riuscita", + "curlCopied": "Comando cURL copiato" }, "layout": { "collections": "Collezioni", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.esempio.com/v1/...", "sending": "Invio...", "send": "Inviare", - "invalidJsonBodyHelp": "Impossibile inviare: il corpo JSON non è valido" + "invalidJsonBodyHelp": "Impossibile inviare: il corpo JSON non è valido", + "copyCurl": "Copia come cURL" }, "requestTabs": { "params": "Param", @@ -1915,7 +2002,10 @@ "placeholderName": "La mia richiesta", "labelFolder": "Cartella", "placeholderFolder": "Seleziona una cartella", - "save": "Salva" + "save": "Salva", + "newFolder": "Nuova cartella", + "newFolderPlaceholder": "Nome cartella", + "create": "Crea" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Esecuzione query…", "emptyTitle": "Esegui una query per vedere i risultati", "emptyHint": "Premi ⌘↩ o clicca Esegui", - "toastNoConnection": "Nessuna connessione attiva." + "toastNoConnection": "Nessuna connessione attiva.", + "btnHistory": "Cronologia", + "btnSaveQuery": "Salva query", + "savedSection": "Salvate", + "recentSection": "Recenti", + "emptyHistory": "Ancora niente — esegui o salva una query", + "savePlaceholder": "Nome query", + "deleteSaved": "Elimina", + "toastQuerySaved": "Query salvata" }, "results": { "filterPlaceholder": "Filtra risultati…", + "editHint": "Fai doppio clic su una cella per modificare", + "toastRowUpdated": "Riga aggiornata", + "toastRowsUpdated": "{count, plural, one {# riga aggiornata} other {# righe aggiornate}}", "rowCount": "{count} {count, plural, one {riga} other {righe}}", "rowCountFiltered": "{filtered} / {total} righe", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ja.json b/apps/desktop-ui/messages/ja.json index 8beb3104..471da4fe 100644 --- a/apps/desktop-ui/messages/ja.json +++ b/apps/desktop-ui/messages/ja.json @@ -1580,7 +1580,13 @@ "cancel": "キャンセル", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {#件のコレクションの削除に失敗しました} other {#件のコレクションの削除に失敗しました}}", + "bulkDeleted": "{count, plural, one {#件のコレクションを削除しました} other {#件のコレクションを削除しました}}", + "bulkDeleteButton": "{count, plural, one {#件のコレクションを削除} other {#件のコレクションを削除}}", + "bulkDeleteFailed": "{count, plural, other {# 件のコレクションの削除に失敗しました}}", + "bulkDeleted": "{count, plural, other {# 件のコレクションを削除しました}}", + "bulkDeleteButton": "{count, plural, other {# 件のコレクション}}を削除" }, "document": { "docsBreadcrumb": "{n}件のドキュメント", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} 件返却", "explainDocsExamined": "{n} 件検査", "explainCollscanHint": "このクエリはコレクション内の全ドキュメントをスキャンします。フィルター対象のフィールドにインデックスの作成を検討してください。", - "explainRawLabel": "explain出力" + "explainRawLabel": "explain出力", + "docInsertFailed": "ドキュメントの挿入に失敗しました", + "docUpdateFailed": "ドキュメントの更新に失敗しました", + "indexesLoadFail": "インデックスの読み込みに失敗しました", + "bulkDeleted": "{count, plural, one {#件のドキュメントを削除しました} other {#件のドキュメントを削除しました}}", + "bulkDeleteFailed": "一括削除に失敗しました", + "selectedCount": "{count, plural, one {#件のドキュメントを選択中} other {#件のドキュメントを選択中}}", + "deleteSelected": "選択項目を削除", + "clearSelection": "クリア", + "statusLoading": "読み込み中…", + "statusShowing": "{total}件中 {from}–{to} 件を表示", + "statusEmpty": "0件のドキュメント", + "statusSelected": "{count}件選択中", + "bulkDeleteTitle": "{count, plural, one {#件のドキュメント} other {#件のドキュメント}}を削除しますか?", + "bulkDeleteDescription": "{collection} から {count, plural, one {#件のドキュメント} other {#件のドキュメント}}を完全に削除します。この操作は元に戻せません。", + "bulkDeleting": "削除中…", + "bulkDeleteConfirm": "すべて削除", + "queryErrorTitle": "ドキュメントの読み込みに失敗しました", + "retry": "再試行", + "docInsertFailed": "ドキュメントの挿入に失敗しました", + "docUpdateFailed": "ドキュメントの更新に失敗しました", + "indexesLoadFail": "インデックスの読み込みに失敗しました", + "bulkDeleted": "{count, plural, other {# 件のドキュメントを削除しました}}", + "bulkDeleteFailed": "一括削除に失敗しました", + "selectedCount": "{count, plural, other {# 件のドキュメントを選択中}}", + "deleteSelected": "選択項目を削除", + "clearSelection": "クリア", + "statusLoading": "読み込み中...", + "statusShowing": "{total} 件中 {from}〜{to} 件を表示", + "statusEmpty": "0 件のドキュメント", + "statusSelected": "{count} 件を選択中", + "bulkDeleteTitle": "{count, plural, other {# 件のドキュメント}}を削除しますか?", + "bulkDeleteDescription": "{collection} から{count, plural, other {# 件のドキュメント}}を完全に削除します。この操作は元に戻せません。", + "bulkDeleting": "削除中...", + "bulkDeleteConfirm": "すべて削除", + "queryErrorTitle": "ドキュメントの読み込みに失敗しました", + "retry": "再試行" }, "tabs": { "filterActive": "アクティブなフィルターが適用されています", @@ -1733,7 +1775,9 @@ "cancel": "キャンセル", "export": "エクスポート", "sheetName": "データ", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "現在のページのみをエクスポートします({count, plural, one {#件のドキュメント} other {#件のドキュメント}})", + "pageOnlyNote": "現在のページのみをエクスポートします({count, plural, other {# 件のドキュメント}})" }, "jsonTree": { "typeLabel": "型:{type}", @@ -1748,6 +1792,132 @@ "previewResult": "プレビュー結果({count})", "previewEmpty": "このステージにドキュメントはありません", "previewFail": "プレビューに失敗しました" + }, + "indexManager": { + "loading": "インデックスを読み込み中…", + "retry": "再試行", + "countLabel": "{count, plural, one {#個のインデックス} other {#個のインデックス}}", + "totalSize": "合計 {size}", + "statsDocs": "{count, plural, one {#件} other {#件}}", + "statsStorage": "{size} ストレージ", + "statsAvgObj": "{size} 平均/件", + "newIndex": "新規インデックス", + "createTitle": "インデックスを作成", + "fieldPlaceholder": "フィールド名", + "ascending": "昇順", + "descending": "降順", + "addField": "フィールドを追加", + "unique": "一意", + "sparse": "スパース", + "ttlLabel": "TTL(秒)", + "ttlPlaceholder": "例: 3600", + "cancel": "キャンセル", + "create": "作成", + "creating": "作成中…", + "created": "インデックスを作成しました", + "createFailed": "インデックスの作成に失敗しました", + "fieldRequired": "フィールド名は必須です", + "dropped": "インデックス「{name}」を削除しました", + "dropFailed": "インデックスの削除に失敗しました", + "dropTitle": "インデックスを削除しますか?", + "dropDescription": "インデックス「{name}」を完全に削除します。このインデックスを使用するクエリは遅くなります。", + "dropping": "削除中…", + "dropConfirm": "インデックスを削除", + "badgeSystem": "システム", + "badgeUnique": "一意", + "badgeSparse": "スパース", + "badgeTtl": "TTL", + "empty": "インデックスが見つかりません" + }, + "importDialog": { + "title": "{collection} にドキュメントをインポート", + "onlyJson": ".json ファイルのみ対応しています", + "invalidStructure": "ファイルには JSON 配列またはオブジェクトが必要です", + "invalidJson": "無効な JSON: ファイルを解析できませんでした", + "imported": "{count, plural, one {#件のドキュメントをインポートしました} other {#件のドキュメントをインポートしました}}", + "importFailed": "インポートに失敗しました", + "dropHint": "ドラッグ&ドロップまたはクリックしてアップロード", + "dropSubHint": "JSON 配列または NDJSON に対応", + "docsCount": "{count, plural, one {#件} other {#件}}", + "previewLabel": "プレビュー(最初の3件)", + "moreDocs": "… 他 {count} 件のドキュメント", + "cancel": "キャンセル", + "importing": "インポート中…", + "importCount": "{count, plural, one {#件をインポート} other {#件をインポート}}", + "importBtn": "インポート" + }, + "schemaView": { + "analyzing": "スキーマを分析中…", + "loadFailed": "スキーマの分析に失敗しました", + "retry": "再試行", + "noDocs": "分析するドキュメントがありません", + "sampled": "{docs}件のドキュメントをサンプリング · {fields}個のフィールド", + "colField": "フィールド", + "colTypes": "型", + "colCoverage": "カバレッジ" + }, + "indexManager": { + "loading": "インデックスを読み込み中...", + "retry": "再試行", + "countLabel": "{count, plural, other {# 件のインデックス}}", + "totalSize": "合計 {size}", + "statsDocs": "{count, plural, other {# 件}}", + "statsStorage": "{size} ストレージ", + "statsAvgObj": "{size} 平均/件", + "newIndex": "新規インデックス", + "createTitle": "インデックスを作成", + "fieldPlaceholder": "フィールド名", + "ascending": "昇順", + "descending": "降順", + "addField": "フィールドを追加", + "unique": "一意", + "sparse": "スパース", + "ttlLabel": "TTL(秒)", + "ttlPlaceholder": "例: 3600", + "cancel": "キャンセル", + "create": "作成", + "creating": "作成中...", + "created": "インデックスを作成しました", + "createFailed": "インデックスの作成に失敗しました", + "fieldRequired": "フィールド名は必須です", + "dropped": "インデックス \"{name}\" を削除しました", + "dropFailed": "インデックスの削除に失敗しました", + "dropTitle": "インデックスを削除しますか?", + "dropDescription": "インデックス \"{name}\" を完全に削除します。このインデックスを使用するクエリは遅くなります。", + "dropping": "削除中...", + "dropConfirm": "インデックスを削除", + "badgeSystem": "システム", + "badgeUnique": "一意", + "badgeSparse": "スパース", + "badgeTtl": "TTL", + "empty": "インデックスが見つかりません" + }, + "importDialog": { + "title": "{collection} にドキュメントをインポート", + "onlyJson": ".json ファイルのみ対応しています", + "invalidStructure": "ファイルには JSON 配列またはオブジェクトを含める必要があります", + "invalidJson": "無効な JSON: ファイルを解析できませんでした", + "imported": "{count, plural, other {# 件のドキュメントをインポートしました}}", + "importFailed": "インポートに失敗しました", + "dropHint": "ドラッグ&ドロップまたはクリックしてアップロード", + "dropSubHint": "JSON 配列または NDJSON に対応", + "docsCount": "{count, plural, other {# 件}}", + "previewLabel": "プレビュー(最初の 3 件)", + "moreDocs": "... 他 {count} 件のドキュメント", + "cancel": "キャンセル", + "importing": "インポート中...", + "importCount": "{count, plural, other {# 件をインポート}}", + "importBtn": "インポート" + }, + "schemaView": { + "analyzing": "スキーマを分析中...", + "loadFailed": "スキーマの分析に失敗しました", + "retry": "再試行", + "noDocs": "分析するドキュメントがありません", + "sampled": "{docs} 件のドキュメントをサンプリング · {fields} 個のフィールド", + "colField": "フィールド", + "colTypes": "型", + "colCoverage": "カバレッジ" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURLが貼り付けられ、正常に解析されました", "responseCopied": "レスポンスをクリップボードにコピーしました", "codeCopied": "コードをクリップボードにコピーしました", - "copyFailed": "クリップボードへのコピーに失敗しました" + "copyFailed": "クリップボードへのコピーに失敗しました", + "curlCopied": "cURLコマンドをコピーしました" }, "layout": { "collections": "コレクション", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "送信中...", "send": "送信", - "invalidJsonBodyHelp": "送信できません: JSONボディが無効です" + "invalidJsonBodyHelp": "送信できません: JSONボディが無効です", + "copyCurl": "cURLとしてコピー" }, "requestTabs": { "params": "パラメータ", @@ -1915,7 +2087,10 @@ "placeholderName": "マイリクエスト", "labelFolder": "フォルダー", "placeholderFolder": "フォルダーを選択", - "save": "保存" + "save": "保存", + "newFolder": "新しいフォルダー", + "newFolderPlaceholder": "フォルダー名", + "create": "作成" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "クエリを実行中…", "emptyTitle": "クエリを実行して結果を表示", "emptyHint": "⌘↩ を押すか実行をクリック", - "toastNoConnection": "アクティブな接続がありません。" + "toastNoConnection": "アクティブな接続がありません。", + "btnHistory": "履歴", + "btnSaveQuery": "クエリを保存", + "savedSection": "保存済み", + "recentSection": "最近", + "emptyHistory": "まだ何もありません — クエリを実行または保存してください", + "savePlaceholder": "クエリ名", + "deleteSaved": "削除", + "toastQuerySaved": "クエリを保存しました" }, "results": { "filterPlaceholder": "結果をフィルタ…", + "editHint": "セルをダブルクリックで編集", + "toastRowUpdated": "行を更新しました", + "toastRowsUpdated": "{count, plural, other {#行を更新しました}}", "rowCount": "{count} 行", "rowCountFiltered": "{filtered} / {total} 行", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ko.json b/apps/desktop-ui/messages/ko.json index b111dad5..39581cf4 100644 --- a/apps/desktop-ui/messages/ko.json +++ b/apps/desktop-ui/messages/ko.json @@ -1580,7 +1580,13 @@ "cancel": "취소", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {컬렉션 #개 삭제 실패} other {컬렉션 #개 삭제 실패}}", + "bulkDeleted": "{count, plural, one {컬렉션 #개 삭제됨} other {컬렉션 #개 삭제됨}}", + "bulkDeleteButton": "{count, plural, one {컬렉션 #개 삭제} other {컬렉션 #개 삭제}}", + "bulkDeleteFailed": "{count, plural, other {컬렉션 # 개 삭제 실패}}", + "bulkDeleted": "{count, plural, other {컬렉션 # 개 삭제됨}}", + "bulkDeleteButton": "{count, plural, other {컬렉션 # 개}} 삭제" }, "document": { "docsBreadcrumb": "{n}개 문서", @@ -1657,7 +1663,43 @@ "explainReturned": "{n}건 반환", "explainDocsExamined": "{n}건 검사", "explainCollscanHint": "이 쿼리는 컬렉션의 모든 문서를 스캔합니다. 필터링된 필드에 인덱스 생성을 고려하세요.", - "explainRawLabel": "explain 출력" + "explainRawLabel": "explain 출력", + "docInsertFailed": "문서 삽입 실패", + "docUpdateFailed": "문서 업데이트 실패", + "indexesLoadFail": "인덱스 로드 실패", + "bulkDeleted": "{count, plural, one {문서 #개 삭제됨} other {문서 #개 삭제됨}}", + "bulkDeleteFailed": "일괄 삭제 실패", + "selectedCount": "{count, plural, one {문서 #개 선택됨} other {문서 #개 선택됨}}", + "deleteSelected": "선택 항목 삭제", + "clearSelection": "지우기", + "statusLoading": "로드 중…", + "statusShowing": "{total}개 중 {from}–{to}개 표시", + "statusEmpty": "문서 0개", + "statusSelected": "{count}개 선택됨", + "bulkDeleteTitle": "문서 {count, plural, one {#개} other {#개}}를 삭제하시겠습니까?", + "bulkDeleteDescription": "{collection}에서 문서 {count, plural, one {#개} other {#개}}를 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다.", + "bulkDeleting": "삭제 중…", + "bulkDeleteConfirm": "모두 삭제", + "queryErrorTitle": "문서 로드 실패", + "retry": "다시 시도", + "docInsertFailed": "문서를 삽입하지 못했습니다", + "docUpdateFailed": "문서를 업데이트하지 못했습니다", + "indexesLoadFail": "인덱스를 불러오지 못했습니다", + "bulkDeleted": "{count, plural, other {문서 # 개 삭제됨}}", + "bulkDeleteFailed": "일괄 삭제 실패", + "selectedCount": "{count, plural, other {문서 # 개 선택됨}}", + "deleteSelected": "선택 항목 삭제", + "clearSelection": "지우기", + "statusLoading": "불러오는 중...", + "statusShowing": "{total}개 중 {from}–{to}개 표시", + "statusEmpty": "문서 0개", + "statusSelected": "{count}개 선택됨", + "bulkDeleteTitle": "{count, plural, other {문서 # 개}}를 삭제하시겠습니까?", + "bulkDeleteDescription": "{collection}에서 {count, plural, other {문서 # 개}}를 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다.", + "bulkDeleting": "삭제하는 중...", + "bulkDeleteConfirm": "모두 삭제", + "queryErrorTitle": "문서를 불러오지 못했습니다", + "retry": "다시 시도" }, "tabs": { "filterActive": "활성 필터 적용됨", @@ -1733,7 +1775,9 @@ "cancel": "취소", "export": "내보내기", "sheetName": "데이터", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "현재 페이지만 내보냅니다({count, plural, one {문서 #개} other {문서 #개}})", + "pageOnlyNote": "현재 페이지만 내보냅니다 ({count, plural, other {문서 # 개}})" }, "jsonTree": { "typeLabel": "타입: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "미리보기 결과 ({count})", "previewEmpty": "이 단계에 문서가 없습니다", "previewFail": "미리보기 실패" + }, + "indexManager": { + "loading": "인덱스 로드 중…", + "retry": "다시 시도", + "countLabel": "{count, plural, one {인덱스 #개} other {인덱스 #개}}", + "totalSize": "총 {size}", + "statsDocs": "{count, plural, one {문서 #개} other {문서 #개}}", + "statsStorage": "{size} 저장소", + "statsAvgObj": "문서당 평균 {size}", + "newIndex": "새 인덱스", + "createTitle": "인덱스 생성", + "fieldPlaceholder": "필드 이름", + "ascending": "오름차순", + "descending": "내림차순", + "addField": "필드 추가", + "unique": "고유", + "sparse": "희소", + "ttlLabel": "TTL(초)", + "ttlPlaceholder": "예: 3600", + "cancel": "취소", + "create": "생성", + "creating": "생성 중…", + "created": "인덱스가 생성됨", + "createFailed": "인덱스 생성 실패", + "fieldRequired": "필드 이름은 필수입니다", + "dropped": "인덱스 \"{name}\"이(가) 삭제됨", + "dropFailed": "인덱스 삭제 실패", + "dropTitle": "인덱스를 삭제하시겠습니까?", + "dropDescription": "인덱스 \"{name}\"을(를) 영구적으로 삭제합니다. 이 인덱스를 사용하는 쿼리가 느려집니다.", + "dropping": "삭제 중…", + "dropConfirm": "인덱스 삭제", + "badgeSystem": "시스템", + "badgeUnique": "고유", + "badgeSparse": "희소", + "badgeTtl": "TTL", + "empty": "인덱스를 찾을 수 없음" + }, + "importDialog": { + "title": "{collection}에 문서 가져오기", + "onlyJson": ".json 파일만 지원됩니다", + "invalidStructure": "파일에는 JSON 배열 또는 객체가 있어야 합니다", + "invalidJson": "잘못된 JSON: 파일을 파싱할 수 없습니다", + "imported": "{count, plural, one {문서 #개 가져옴} other {문서 #개 가져옴}}", + "importFailed": "가져오기 실패", + "dropHint": "끌어다 놓거나 클릭하여 업로드", + "dropSubHint": "JSON 배열 또는 NDJSON 지원", + "docsCount": "{count, plural, one {문서 #개} other {문서 #개}}", + "previewLabel": "미리보기(처음 3개 문서)", + "moreDocs": "… 외 {count}개 문서", + "cancel": "취소", + "importing": "가져오는 중…", + "importCount": "{count, plural, one {문서 #개 가져오기} other {문서 #개 가져오기}}", + "importBtn": "가져오기" + }, + "schemaView": { + "analyzing": "스키마 분석 중…", + "loadFailed": "스키마 분석 실패", + "retry": "다시 시도", + "noDocs": "분석할 문서가 없습니다", + "sampled": "문서 {docs}개 샘플링 · 필드 {fields}개", + "colField": "필드", + "colTypes": "유형", + "colCoverage": "커버리지" + }, + "indexManager": { + "loading": "인덱스를 불러오는 중...", + "retry": "다시 시도", + "countLabel": "{count, plural, other {인덱스 # 개}}", + "totalSize": "총 {size}", + "statsDocs": "{count, plural, other {문서 # 개}}", + "statsStorage": "{size} 저장소", + "statsAvgObj": "{size} 평균/문서", + "newIndex": "새 인덱스", + "createTitle": "인덱스 생성", + "fieldPlaceholder": "필드 이름", + "ascending": "오름차순", + "descending": "내림차순", + "addField": "필드 추가", + "unique": "고유", + "sparse": "희소", + "ttlLabel": "TTL(초)", + "ttlPlaceholder": "예: 3600", + "cancel": "취소", + "create": "생성", + "creating": "생성하는 중...", + "created": "인덱스가 생성됨", + "createFailed": "인덱스를 생성하지 못했습니다", + "fieldRequired": "필드 이름은 필수입니다", + "dropped": "인덱스 \"{name}\"이(가) 삭제됨", + "dropFailed": "인덱스를 삭제하지 못했습니다", + "dropTitle": "인덱스를 삭제하시겠습니까?", + "dropDescription": "인덱스 \"{name}\"을(를) 영구적으로 삭제합니다. 이 인덱스를 사용하는 쿼리가 느려집니다.", + "dropping": "삭제하는 중...", + "dropConfirm": "인덱스 삭제", + "badgeSystem": "시스템", + "badgeUnique": "고유", + "badgeSparse": "희소", + "badgeTtl": "TTL", + "empty": "인덱스를 찾을 수 없습니다" + }, + "importDialog": { + "title": "{collection}(으)로 문서 가져오기", + "onlyJson": ".json 파일만 지원됩니다", + "invalidStructure": "파일에는 JSON 배열 또는 객체가 있어야 합니다", + "invalidJson": "잘못된 JSON: 파일을 구문 분석할 수 없습니다", + "imported": "{count, plural, other {문서 # 개 가져옴}}", + "importFailed": "가져오기 실패", + "dropHint": "끌어다 놓거나 클릭하여 업로드", + "dropSubHint": "JSON 배열 또는 NDJSON 지원", + "docsCount": "{count, plural, other {문서 # 개}}", + "previewLabel": "미리보기(처음 3개 문서)", + "moreDocs": "... 외 {count}개 문서", + "cancel": "취소", + "importing": "가져오는 중...", + "importCount": "{count, plural, other {문서 # 개 가져오기}}", + "importBtn": "가져오기" + }, + "schemaView": { + "analyzing": "스키마 분석 중...", + "loadFailed": "스키마를 분석하지 못했습니다", + "retry": "다시 시도", + "noDocs": "분석할 문서가 없습니다", + "sampled": "{docs}개 문서 샘플링 · {fields}개 필드", + "colField": "필드", + "colTypes": "유형", + "colCoverage": "커버리지" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL이 붙여넣기 및 파싱되었습니다", "responseCopied": "응답이 클립보드에 복사되었습니다", "codeCopied": "코드가 클립보드에 복사되었습니다", - "copyFailed": "클립보드에 복사하지 못했습니다" + "copyFailed": "클립보드에 복사하지 못했습니다", + "curlCopied": "cURL 명령이 복사되었습니다" }, "layout": { "collections": "컬렉션", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "전송 중...", "send": "전송", - "invalidJsonBodyHelp": "전송 불가: JSON 본문이 유효하지 않습니다" + "invalidJsonBodyHelp": "전송 불가: JSON 본문이 유효하지 않습니다", + "copyCurl": "cURL로 복사" }, "requestTabs": { "params": "매개변수", @@ -1915,7 +2087,10 @@ "placeholderName": "내 요청", "labelFolder": "폴더", "placeholderFolder": "폴더를 선택하세요", - "save": "저장" + "save": "저장", + "newFolder": "새 폴더", + "newFolderPlaceholder": "폴더 이름", + "create": "생성" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "쿼리 실행 중…", "emptyTitle": "쿼리를 실행하여 결과 보기", "emptyHint": "⌘↩를 누르거나 실행 클릭", - "toastNoConnection": "활성 연결 없음." + "toastNoConnection": "활성 연결 없음.", + "btnHistory": "기록", + "btnSaveQuery": "쿼리 저장", + "savedSection": "저장됨", + "recentSection": "최근", + "emptyHistory": "아직 없습니다 — 쿼리를 실행하거나 저장하세요", + "savePlaceholder": "쿼리 이름", + "deleteSaved": "삭제", + "toastQuerySaved": "쿼리가 저장되었습니다" }, "results": { "filterPlaceholder": "결과 필터…", + "editHint": "셀을 더블클릭하여 편집", + "toastRowUpdated": "행이 업데이트되었습니다", + "toastRowsUpdated": "{count, plural, other {#개 행이 업데이트되었습니다}}", "rowCount": "{count}행", "rowCountFiltered": "{filtered} / {total}행", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ms.json b/apps/desktop-ui/messages/ms.json index a077fefa..15e1a393 100644 --- a/apps/desktop-ui/messages/ms.json +++ b/apps/desktop-ui/messages/ms.json @@ -1580,7 +1580,10 @@ "cancel": "Batal", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Gagal memadam # koleksi} other {Gagal memadam # koleksi}}", + "bulkDeleted": "{count, plural, one {# koleksi dipadam} other {# koleksi dipadam}}", + "bulkDeleteButton": "Padam {count, plural, one {# Koleksi} other {# Koleksi}}" }, "document": { "docsBreadcrumb": "{n} dokumen", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} dikembalikan", "explainDocsExamined": "{n} diperiksa", "explainCollscanHint": "Pertanyaan ini mengimbas setiap dokumen dalam koleksi. Pertimbangkan untuk mencipta indeks pada medan yang ditapis.", - "explainRawLabel": "Output explain" + "explainRawLabel": "Output explain", + "docInsertFailed": "Gagal menyisipkan dokumen", + "docUpdateFailed": "Gagal mengemas kini dokumen", + "indexesLoadFail": "Gagal memuatkan indeks", + "bulkDeleted": "{count, plural, one {# dokumen dipadam} other {# dokumen dipadam}}", + "bulkDeleteFailed": "Pemadaman pukal gagal", + "selectedCount": "{count, plural, one {# dokumen dipilih} other {# dokumen dipilih}}", + "deleteSelected": "Padam Yang Dipilih", + "clearSelection": "Kosongkan", + "statusLoading": "Memuatkan...", + "statusShowing": "Memaparkan {from}–{to} daripada {total} dokumen", + "statusEmpty": "0 dokumen", + "statusSelected": "{count} dipilih", + "bulkDeleteTitle": "Padam {count, plural, one {# dokumen} other {# dokumen}}?", + "bulkDeleteDescription": "Ini akan memadam {count, plural, one {# dokumen} other {# dokumen}} daripada {collection} secara kekal. Tindakan ini tidak boleh dibuat asal.", + "bulkDeleting": "Memadam...", + "bulkDeleteConfirm": "Padam Semua", + "queryErrorTitle": "Gagal memuatkan dokumen", + "retry": "Cuba Semula" }, "tabs": { "filterActive": "Penapis aktif digunakan", @@ -1733,7 +1754,8 @@ "cancel": "Batal", "export": "Eksport", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Mengeksport halaman semasa sahaja ({count, plural, one {# dokumen} other {# dokumen}})" }, "jsonTree": { "typeLabel": "Jenis: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Hasil pratonton ({count})", "previewEmpty": "Tiada dokumen pada peringkat ini", "previewFail": "Pratonton gagal" + }, + "indexManager": { + "loading": "Memuatkan indeks...", + "retry": "Cuba Semula", + "countLabel": "{count, plural, one {# indeks} other {# indeks}}", + "totalSize": "{size} jumlah", + "statsDocs": "{count, plural, one {# dok} other {# dok}}", + "statsStorage": "{size} storan", + "statsAvgObj": "{size} purata/dok", + "newIndex": "Indeks Baharu", + "createTitle": "Cipta Indeks", + "fieldPlaceholder": "Nama medan", + "ascending": "Menaik", + "descending": "Menurun", + "addField": "Tambah medan", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (saat)", + "ttlPlaceholder": "cth. 3600", + "cancel": "Batal", + "create": "Cipta", + "creating": "Mencipta...", + "created": "Indeks dicipta", + "createFailed": "Gagal mencipta indeks", + "fieldRequired": "Nama medan diperlukan", + "dropped": "Indeks \"{name}\" digugurkan", + "dropFailed": "Gagal menggugurkan indeks", + "dropTitle": "Gugurkan indeks?", + "dropDescription": "Ini akan menggugurkan indeks \"{name}\" secara kekal. Pertanyaan yang menggunakan indeks ini akan menjadi perlahan.", + "dropping": "Menggugurkan...", + "dropConfirm": "Gugurkan Indeks", + "badgeSystem": "sistem", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Tiada indeks ditemui" + }, + "importDialog": { + "title": "Import Dokumen ke dalam {collection}", + "onlyJson": "Hanya fail .json disokong", + "invalidStructure": "Fail mesti mengandungi tatasusunan atau objek JSON", + "invalidJson": "JSON tidak sah: fail tidak dapat dihurai", + "imported": "{count, plural, one {# dokumen diimport} other {# dokumen diimport}}", + "importFailed": "Import gagal", + "dropHint": "Seret & lepas atau klik untuk muat naik", + "dropSubHint": "Menyokong tatasusunan JSON atau NDJSON", + "docsCount": "{count, plural, one {# dok} other {# dok}}", + "previewLabel": "Pratonton (3 dokumen pertama)", + "moreDocs": "... dan {count} lagi dokumen", + "cancel": "Batal", + "importing": "Mengimport...", + "importCount": "{count, plural, one {Import # Dok} other {Import # Dok}}", + "importBtn": "Import" + }, + "schemaView": { + "analyzing": "Menganalisis skema...", + "loadFailed": "Gagal menganalisis skema", + "retry": "Cuba Semula", + "noDocs": "Tiada dokumen untuk dianalisis", + "sampled": "Sampel {docs} dokumen · {fields} medan", + "colField": "Medan", + "colTypes": "Jenis", + "colCoverage": "Liputan" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL ditampal dan dihurai dengan berjaya", "responseCopied": "Respons disalin ke papan klip", "codeCopied": "Kod disalin ke papan klip", - "copyFailed": "Gagal menyalin ke papan klip" + "copyFailed": "Gagal menyalin ke papan klip", + "curlCopied": "Perintah cURL disalin" }, "layout": { "collections": "Koleksi", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.contoh.com/v1/...", "sending": "Menghantar...", "send": "Hantar", - "invalidJsonBodyHelp": "Tidak boleh hantar: kandungan JSON tidak sah" + "invalidJsonBodyHelp": "Tidak boleh hantar: kandungan JSON tidak sah", + "copyCurl": "Salin sebagai cURL" }, "requestTabs": { "params": "Parameter", @@ -1915,7 +2002,10 @@ "placeholderName": "Permintaan saya", "labelFolder": "Folder", "placeholderFolder": "Pilih folder", - "save": "Simpan" + "save": "Simpan", + "newFolder": "Folder baharu", + "newFolderPlaceholder": "Nama folder", + "create": "Cipta" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Menjalankan pertanyaan…", "emptyTitle": "Jalankan pertanyaan untuk melihat keputusan", "emptyHint": "Tekan ⌘↩ atau klik Jalankan", - "toastNoConnection": "Tiada sambungan aktif." + "toastNoConnection": "Tiada sambungan aktif.", + "btnHistory": "Sejarah", + "btnSaveQuery": "Simpan pertanyaan", + "savedSection": "Disimpan", + "recentSection": "Terkini", + "emptyHistory": "Belum ada apa-apa — jalankan atau simpan pertanyaan", + "savePlaceholder": "Nama pertanyaan", + "deleteSaved": "Padam", + "toastQuerySaved": "Pertanyaan disimpan" }, "results": { "filterPlaceholder": "Tapis keputusan…", + "editHint": "Klik dua kali sel untuk mengedit", + "toastRowUpdated": "Baris dikemas kini", + "toastRowsUpdated": "{count, plural, other {# baris dikemas kini}}", "rowCount": "{count} baris", "rowCountFiltered": "{filtered} / {total} baris", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/nb.json b/apps/desktop-ui/messages/nb.json index b93be091..db3c3c56 100644 --- a/apps/desktop-ui/messages/nb.json +++ b/apps/desktop-ui/messages/nb.json @@ -1580,7 +1580,13 @@ "cancel": "Avbryt", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Kunne ikke slette # samling} other {Kunne ikke slette # samlinger}}", + "bulkDeleted": "{count, plural, one {# samling slettet} other {# samlinger slettet}}", + "bulkDeleteButton": "{count, plural, one {Slett # samling} other {Slett # samlinger}}", + "bulkDeleteFailed": "{count, plural, one {Kunne ikke slette # samling} other {Kunne ikke slette # samlinger}}", + "bulkDeleted": "{count, plural, one {Slettet # samling} other {Slettet # samlinger}}", + "bulkDeleteButton": "Slett {count, plural, one {# samling} other {# samlinger}}" }, "document": { "docsBreadcrumb": "{n} dok.", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} returnert", "explainDocsExamined": "{n} undersøkt", "explainCollscanHint": "Denne spørringen skanner alle dokumenter i collectionen. Vurder å opprette en indeks på de filtrerte feltene.", - "explainRawLabel": "Explain-utdata" + "explainRawLabel": "Explain-utdata", + "docInsertFailed": "Kunne ikke sette inn dokument", + "docUpdateFailed": "Kunne ikke oppdatere dokument", + "indexesLoadFail": "Kunne ikke laste indekser", + "bulkDeleted": "{count, plural, one {# dokument slettet} other {# dokumenter slettet}}", + "bulkDeleteFailed": "Massesletting mislyktes", + "selectedCount": "{count, plural, one {# dokument valgt} other {# dokumenter valgt}}", + "deleteSelected": "Slett valgte", + "clearSelection": "Tøm", + "statusLoading": "Laster…", + "statusShowing": "{from}-{to} av {total} dokumenter", + "statusEmpty": "0 dokumenter", + "statusSelected": "{count} valgt", + "bulkDeleteTitle": "Slette {count, plural, one {# dokument} other {# dokumenter}}?", + "bulkDeleteDescription": "Dette sletter permanent {count, plural, one {# dokument} other {# dokumenter}} fra {collection}. Dette kan ikke angres.", + "bulkDeleting": "Sletter…", + "bulkDeleteConfirm": "Slett alle", + "queryErrorTitle": "Kunne ikke laste dokumenter", + "retry": "Prøv igjen", + "docInsertFailed": "Kunne ikke sette inn dokumentet", + "docUpdateFailed": "Kunne ikke oppdatere dokumentet", + "indexesLoadFail": "Kunne ikke laste inn indeksene", + "bulkDeleted": "{count, plural, one {# dokument slettet} other {# dokumenter slettet}}", + "bulkDeleteFailed": "Massesletting mislyktes", + "selectedCount": "{count, plural, one {# dokument valgt} other {# dokumenter valgt}}", + "deleteSelected": "Slett valgte", + "clearSelection": "Tøm", + "statusLoading": "Laster inn …", + "statusShowing": "Viser {from}-{to} av {total} dokumenter", + "statusEmpty": "0 dokumenter", + "statusSelected": "{count} valgt", + "bulkDeleteTitle": "Slette {count, plural, one {# dokument} other {# dokumenter}}?", + "bulkDeleteDescription": "Dette sletter permanent {count, plural, one {# dokument} other {# dokumenter}} fra {collection}. Dette kan ikke angres.", + "bulkDeleting": "Sletter …", + "bulkDeleteConfirm": "Slett alle", + "queryErrorTitle": "Kunne ikke laste inn dokumentene", + "retry": "Prøv igjen" }, "tabs": { "filterActive": "Aktivt filter brukt", @@ -1733,7 +1775,9 @@ "cancel": "Avbryt", "export": "Eksporter", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Eksporterer kun gjeldende side ({count, plural, one {# dokument} other {# dokumenter}})", + "pageOnlyNote": "Eksporterer bare gjeldende side ({count, plural, one {# dokument} other {# dokumenter}})" }, "jsonTree": { "typeLabel": "Type: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Forhåndsvisningsresultater ({count})", "previewEmpty": "Ingen dokumenter på dette trinnet", "previewFail": "Forhåndsvisning mislyktes" + }, + "indexManager": { + "loading": "Laster indekser…", + "retry": "Prøv igjen", + "countLabel": "{count, plural, one {# indeks} other {# indekser}}", + "totalSize": "{size} totalt", + "statsDocs": "{count, plural, one {# dok.} other {# dok.}}", + "statsStorage": "{size} lagring", + "statsAvgObj": "{size} gj.sn./dok.", + "newIndex": "Nytt indeks", + "createTitle": "Opprett indeks", + "fieldPlaceholder": "Feltnavn", + "ascending": "Stigende", + "descending": "Synkende", + "addField": "Legg til felt", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (sekunder)", + "ttlPlaceholder": "f.eks. 3600", + "cancel": "Avbryt", + "create": "Opprett", + "creating": "Oppretter…", + "created": "Indeks opprettet", + "createFailed": "Kunne ikke opprette indeks", + "fieldRequired": "Feltnavn er påkrevd", + "dropped": "Indeks \"{name}\" slettet", + "dropFailed": "Kunne ikke slette indeks", + "dropTitle": "Slette indeks?", + "dropDescription": "Dette sletter permanent indekset \"{name}\". Spørringer som bruker dette indekset, blir tregere.", + "dropping": "Sletter…", + "dropConfirm": "Slett indeks", + "badgeSystem": "system", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Ingen indekser funnet" + }, + "importDialog": { + "title": "Importer dokumenter til {collection}", + "onlyJson": "Bare .json-filer støttes", + "invalidStructure": "Filen må inneholde et JSON-array eller -objekt", + "invalidJson": "Ugyldig JSON: kunne ikke parse filen", + "imported": "{count, plural, one {# dokument importert} other {# dokumenter importert}}", + "importFailed": "Import mislyktes", + "dropHint": "Dra og slipp eller klikk for å laste opp", + "dropSubHint": "Støtter JSON-array eller NDJSON", + "docsCount": "{count, plural, one {# dok.} other {# dok.}}", + "previewLabel": "Forhåndsvisning (første 3 dokumenter)", + "moreDocs": "… og {count} dokumenter til", + "cancel": "Avbryt", + "importing": "Importerer…", + "importCount": "{count, plural, one {Importer # dok.} other {Importer # dok.}}", + "importBtn": "Importer" + }, + "schemaView": { + "analyzing": "Analyserer skjema…", + "loadFailed": "Kunne ikke analysere skjema", + "retry": "Prøv igjen", + "noDocs": "Ingen dokumenter å analysere", + "sampled": "{docs} dokumenter analysert · {fields} felt", + "colField": "Felt", + "colTypes": "Typer", + "colCoverage": "Dekning" + }, + "indexManager": { + "loading": "Laster inn indekser …", + "retry": "Prøv igjen", + "countLabel": "{count, plural, one {# indeks} other {# indekser}}", + "totalSize": "{size} totalt", + "statsDocs": "{count, plural, one {# dok.} other {# dok.}}", + "statsStorage": "{size} lagring", + "statsAvgObj": "{size} gj.sn./dok.", + "newIndex": "Ny indeks", + "createTitle": "Opprett indeks", + "fieldPlaceholder": "Feltnavn", + "ascending": "Stigende", + "descending": "Synkende", + "addField": "Legg til felt", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (sekunder)", + "ttlPlaceholder": "f.eks. 3600", + "cancel": "Avbryt", + "create": "Opprett", + "creating": "Oppretter …", + "created": "Indeks opprettet", + "createFailed": "Kunne ikke opprette indeksen", + "fieldRequired": "Feltnavn er påkrevd", + "dropped": "Indeks \"{name}\" fjernet", + "dropFailed": "Kunne ikke fjerne indeksen", + "dropTitle": "Fjerne indeks?", + "dropDescription": "Dette fjerner permanent indeksen \"{name}\". Spørringer som bruker denne indeksen, blir tregere.", + "dropping": "Fjerner …", + "dropConfirm": "Fjern indeks", + "badgeSystem": "system", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Ingen indekser funnet" + }, + "importDialog": { + "title": "Importer dokumenter til {collection}", + "onlyJson": "Bare .json-filer støttes", + "invalidStructure": "Filen må inneholde et JSON-array eller -objekt", + "invalidJson": "Ugyldig JSON: kunne ikke parse filen", + "imported": "{count, plural, one {Importerte # dokument} other {Importerte # dokumenter}}", + "importFailed": "Import mislyktes", + "dropHint": "Dra og slipp eller klikk for å laste opp", + "dropSubHint": "Støtter JSON-array eller NDJSON", + "docsCount": "{count, plural, one {# dok.} other {# dok.}}", + "previewLabel": "Forhåndsvisning (første 3 dokumenter)", + "moreDocs": "… og {count} dokumenter til", + "cancel": "Avbryt", + "importing": "Importerer …", + "importCount": "{count, plural, one {Importer # dok.} other {Importer # dok.}}", + "importBtn": "Importer" + }, + "schemaView": { + "analyzing": "Analyserer skjema …", + "loadFailed": "Kunne ikke analysere skjemaet", + "retry": "Prøv igjen", + "noDocs": "Ingen dokumenter å analysere", + "sampled": "Utvalg av {docs} dokumenter · {fields} felter", + "colField": "Felt", + "colTypes": "Typer", + "colCoverage": "Dekning" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL limt inn og analysert", "responseCopied": "Svar kopiert til utklippstavle", "codeCopied": "Kode kopiert til utklippstavle", - "copyFailed": "Kopiering til utklippstavlen mislyktes" + "copyFailed": "Kopiering til utklippstavlen mislyktes", + "curlCopied": "cURL-kommando kopiert" }, "layout": { "collections": "Samlinger", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.eksempel.no/v1/...", "sending": "Sender...", "send": "Send", - "invalidJsonBodyHelp": "Kan ikke sende: JSON-kroppen er ugyldig" + "invalidJsonBodyHelp": "Kan ikke sende: JSON-kroppen er ugyldig", + "copyCurl": "Kopier som cURL" }, "requestTabs": { "params": "Parametere", @@ -1915,7 +2087,10 @@ "placeholderName": "Min forespørsel", "labelFolder": "Mappe", "placeholderFolder": "Velg en mappe", - "save": "Lagre" + "save": "Lagre", + "newFolder": "Ny mappe", + "newFolderPlaceholder": "Mappenavn", + "create": "Opprett" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Utfører spørring…", "emptyTitle": "Kjør en spørring for å se resultater", "emptyHint": "Trykk ⌘↩ eller klikk Kjør", - "toastNoConnection": "Ingen aktiv tilkobling." + "toastNoConnection": "Ingen aktiv tilkobling.", + "btnHistory": "Historikk", + "btnSaveQuery": "Lagre spørring", + "savedSection": "Lagrede", + "recentSection": "Nylige", + "emptyHistory": "Ingenting ennå — kjør eller lagre en spørring", + "savePlaceholder": "Spørringsnavn", + "deleteSaved": "Slett", + "toastQuerySaved": "Spørring lagret" }, "results": { "filterPlaceholder": "Filtrer resultater…", + "editHint": "Dobbeltklikk på en celle for å redigere", + "toastRowUpdated": "Rad oppdatert", + "toastRowsUpdated": "{count, plural, one {# rad oppdatert} other {# rader oppdatert}}", "rowCount": "{count} {count, plural, one {rad} other {rader}}", "rowCountFiltered": "{filtered} / {total} rader", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/nl.json b/apps/desktop-ui/messages/nl.json index f9adafb5..8e3b0277 100644 --- a/apps/desktop-ui/messages/nl.json +++ b/apps/desktop-ui/messages/nl.json @@ -1580,7 +1580,13 @@ "cancel": "Annuleren", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {# collectie verwijderen mislukt} other {# collecties verwijderen mislukt}}", + "bulkDeleted": "{count, plural, one {# collectie verwijderd} other {# collecties verwijderd}}", + "bulkDeleteButton": "{count, plural, one {# collectie verwijderen} other {# collecties verwijderen}}", + "bulkDeleteFailed": "{count, plural, one {Kan # collectie niet verwijderen} other {Kan # collecties niet verwijderen}}", + "bulkDeleted": "{count, plural, one {# collectie verwijderd} other {# collecties verwijderd}}", + "bulkDeleteButton": "{count, plural, one {# collectie} other {# collecties}} verwijderen" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} geretourneerd", "explainDocsExamined": "{n} onderzocht", "explainCollscanHint": "Deze query doorloopt elk document in de collectie. Overweeg een index op de gefilterde velden.", - "explainRawLabel": "Explain-uitvoer" + "explainRawLabel": "Explain-uitvoer", + "docInsertFailed": "Document invoegen mislukt", + "docUpdateFailed": "Document bijwerken mislukt", + "indexesLoadFail": "Indexen laden mislukt", + "bulkDeleted": "{count, plural, one {# document verwijderd} other {# documenten verwijderd}}", + "bulkDeleteFailed": "Bulkverwijdering mislukt", + "selectedCount": "{count, plural, one {# document geselecteerd} other {# documenten geselecteerd}}", + "deleteSelected": "Geselecteerde verwijderen", + "clearSelection": "Wissen", + "statusLoading": "Laden…", + "statusShowing": "{from}–{to} van {total} documenten", + "statusEmpty": "0 documenten", + "statusSelected": "{count} geselecteerd", + "bulkDeleteTitle": "{count, plural, one {# document} other {# documenten}} verwijderen?", + "bulkDeleteDescription": "Hiermee worden {count, plural, one {# document} other {# documenten}} definitief uit {collection} verwijderd. Dit kan niet ongedaan worden gemaakt.", + "bulkDeleting": "Verwijderen…", + "bulkDeleteConfirm": "Alles verwijderen", + "queryErrorTitle": "Documenten laden mislukt", + "retry": "Opnieuw", + "docInsertFailed": "Kan document niet invoegen", + "docUpdateFailed": "Kan document niet bijwerken", + "indexesLoadFail": "Kan indexen niet laden", + "bulkDeleted": "{count, plural, one {# document verwijderd} other {# documenten verwijderd}}", + "bulkDeleteFailed": "Bulkverwijdering mislukt", + "selectedCount": "{count, plural, one {# document geselecteerd} other {# documenten geselecteerd}}", + "deleteSelected": "Selectie verwijderen", + "clearSelection": "Wissen", + "statusLoading": "Laden …", + "statusShowing": "{from}-{to} van {total} documenten weergegeven", + "statusEmpty": "0 documenten", + "statusSelected": "{count} geselecteerd", + "bulkDeleteTitle": "{count, plural, one {# document} other {# documenten}} verwijderen?", + "bulkDeleteDescription": "Hiermee worden {count, plural, one {# document} other {# documenten}} permanent verwijderd uit {collection}. Dit kan niet ongedaan worden gemaakt.", + "bulkDeleting": "Verwijderen …", + "bulkDeleteConfirm": "Alles verwijderen", + "queryErrorTitle": "Kan documenten niet laden", + "retry": "Opnieuw proberen" }, "tabs": { "filterActive": "Actief filter toegepast", @@ -1733,7 +1775,9 @@ "cancel": "Annuleren", "export": "Exporteren", "sheetName": "Gegevens", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporteert alleen de huidige pagina ({count, plural, one {# document} other {# documenten}})", + "pageOnlyNote": "Exporteert alleen de huidige pagina ({count, plural, one {# document} other {# documenten}})" }, "jsonTree": { "typeLabel": "Type: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Voorbeeldresultaten ({count})", "previewEmpty": "Geen documenten in deze fase", "previewFail": "Voorbeeld mislukt" + }, + "indexManager": { + "loading": "Indexen laden…", + "retry": "Opnieuw", + "countLabel": "{count, plural, one {# index} other {# indexen}}", + "totalSize": "{size} totaal", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} opslag", + "statsAvgObj": "{size} gem./doc", + "newIndex": "Nieuwe index", + "createTitle": "Index maken", + "fieldPlaceholder": "Veldnaam", + "ascending": "Oplopend", + "descending": "Aflopend", + "addField": "Veld toevoegen", + "unique": "Uniek", + "sparse": "Sparse", + "ttlLabel": "TTL (seconden)", + "ttlPlaceholder": "bijv. 3600", + "cancel": "Annuleren", + "create": "Maken", + "creating": "Maken…", + "created": "Index gemaakt", + "createFailed": "Index maken mislukt", + "fieldRequired": "Veldnaam is vereist", + "dropped": "Index \"{name}\" verwijderd", + "dropFailed": "Index verwijderen mislukt", + "dropTitle": "Index verwijderen?", + "dropDescription": "Hiermee wordt index \"{name}\" definitief verwijderd. Query's die deze index gebruiken worden trager.", + "dropping": "Verwijderen…", + "dropConfirm": "Index verwijderen", + "badgeSystem": "systeem", + "badgeUnique": "uniek", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Geen indexen gevonden" + }, + "importDialog": { + "title": "Documenten importeren in {collection}", + "onlyJson": "Alleen .json-bestanden worden ondersteund", + "invalidStructure": "Bestand moet een JSON-array of -object bevatten", + "invalidJson": "Ongeldige JSON: bestand kon niet worden geparseerd", + "imported": "{count, plural, one {# document geïmporteerd} other {# documenten geïmporteerd}}", + "importFailed": "Importeren mislukt", + "dropHint": "Sleep en zet neer of klik om te uploaden", + "dropSubHint": "Ondersteunt JSON-array of NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Voorbeeld (eerste 3 documenten)", + "moreDocs": "… en {count} documenten meer", + "cancel": "Annuleren", + "importing": "Importeren…", + "importCount": "{count, plural, one {# doc importeren} other {# docs importeren}}", + "importBtn": "Importeren" + }, + "schemaView": { + "analyzing": "Schema analyseren…", + "loadFailed": "Schema analyseren mislukt", + "retry": "Opnieuw", + "noDocs": "Geen documenten om te analyseren", + "sampled": "{docs} documenten bemonsterd · {fields} velden", + "colField": "Veld", + "colTypes": "Typen", + "colCoverage": "Dekking" + }, + "indexManager": { + "loading": "Indexen laden …", + "retry": "Opnieuw proberen", + "countLabel": "{count, plural, one {# index} other {# indexen}}", + "totalSize": "{size} totaal", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} opslag", + "statsAvgObj": "{size} gem./doc", + "newIndex": "Nieuwe index", + "createTitle": "Index maken", + "fieldPlaceholder": "Veldnaam", + "ascending": "Oplopend", + "descending": "Aflopend", + "addField": "Veld toevoegen", + "unique": "Uniek", + "sparse": "Sparse", + "ttlLabel": "TTL (seconden)", + "ttlPlaceholder": "bijv. 3600", + "cancel": "Annuleren", + "create": "Maken", + "creating": "Maken …", + "created": "Index gemaakt", + "createFailed": "Kan index niet maken", + "fieldRequired": "Veldnaam is vereist", + "dropped": "Index \"{name}\" verwijderd", + "dropFailed": "Kan index niet verwijderen", + "dropTitle": "Index verwijderen?", + "dropDescription": "Hiermee wordt index \"{name}\" permanent verwijderd. Query's die deze index gebruiken, worden trager.", + "dropping": "Verwijderen …", + "dropConfirm": "Index verwijderen", + "badgeSystem": "systeem", + "badgeUnique": "uniek", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Geen indexen gevonden" + }, + "importDialog": { + "title": "Documenten importeren in {collection}", + "onlyJson": "Alleen .json-bestanden worden ondersteund", + "invalidStructure": "Bestand moet een JSON-array of -object bevatten", + "invalidJson": "Ongeldige JSON: bestand kan niet worden geparseerd", + "imported": "{count, plural, one {# document geïmporteerd} other {# documenten geïmporteerd}}", + "importFailed": "Import mislukt", + "dropHint": "Sleep en zet neer of klik om te uploaden", + "dropSubHint": "Ondersteunt JSON-array of NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Voorbeeld (eerste 3 documenten)", + "moreDocs": "… en nog {count} documenten", + "cancel": "Annuleren", + "importing": "Importeren …", + "importCount": "{count, plural, one {# doc importeren} other {# docs importeren}}", + "importBtn": "Importeren" + }, + "schemaView": { + "analyzing": "Schema analyseren …", + "loadFailed": "Kan schema niet analyseren", + "retry": "Opnieuw proberen", + "noDocs": "Geen documenten om te analyseren", + "sampled": "{docs} documenten bemonsterd · {fields} velden", + "colField": "Veld", + "colTypes": "Typen", + "colCoverage": "Dekking" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL geplakt en succesvol verwerkt", "responseCopied": "Antwoord gekopieerd naar klembord", "codeCopied": "Code gekopieerd naar klembord", - "copyFailed": "Kopiëren naar klembord mislukt" + "copyFailed": "Kopiëren naar klembord mislukt", + "curlCopied": "cURL-opdracht gekopieerd" }, "layout": { "collections": "Collecties", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.voorbeeld.nl/v1/...", "sending": "Verzenden...", "send": "Verzenden", - "invalidJsonBodyHelp": "Kan niet verzenden: de JSON-body is ongeldig" + "invalidJsonBodyHelp": "Kan niet verzenden: de JSON-body is ongeldig", + "copyCurl": "Kopiëren als cURL" }, "requestTabs": { "params": "Parameters", @@ -1915,7 +2087,10 @@ "placeholderName": "Mijn verzoek", "labelFolder": "Map", "placeholderFolder": "Selecteer een map", - "save": "Opslaan" + "save": "Opslaan", + "newFolder": "Nieuwe map", + "newFolderPlaceholder": "Mapnaam", + "create": "Aanmaken" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Query uitvoeren…", "emptyTitle": "Voer een query uit om resultaten te zien", "emptyHint": "Druk op ⌘↩ of klik Uitvoeren", - "toastNoConnection": "Geen actieve verbinding." + "toastNoConnection": "Geen actieve verbinding.", + "btnHistory": "Geschiedenis", + "btnSaveQuery": "Query opslaan", + "savedSection": "Opgeslagen", + "recentSection": "Recent", + "emptyHistory": "Nog niets — voer een query uit of sla er een op", + "savePlaceholder": "Querynaam", + "deleteSaved": "Verwijderen", + "toastQuerySaved": "Query opgeslagen" }, "results": { "filterPlaceholder": "Resultaten filteren…", + "editHint": "Dubbelklik op een cel om te bewerken", + "toastRowUpdated": "Rij bijgewerkt", + "toastRowsUpdated": "{count, plural, one {# rij bijgewerkt} other {# rijen bijgewerkt}}", "rowCount": "{count} {count, plural, one {rij} other {rijen}}", "rowCountFiltered": "{filtered} / {total} rijen", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/pl.json b/apps/desktop-ui/messages/pl.json index 1b8bee2d..4cd12d26 100644 --- a/apps/desktop-ui/messages/pl.json +++ b/apps/desktop-ui/messages/pl.json @@ -1580,7 +1580,13 @@ "cancel": "Cancel", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Nie udało się usunąć # kolekcji} few {Nie udało się usunąć # kolekcji} many {Nie udało się usunąć # kolekcji} other {Nie udało się usunąć # kolekcji}}", + "bulkDeleted": "{count, plural, one {Usunięto # kolekcję} few {Usunięto # kolekcje} many {Usunięto # kolekcji} other {Usunięto # kolekcji}}", + "bulkDeleteButton": "{count, plural, one {Usuń # kolekcję} few {Usuń # kolekcje} many {Usuń # kolekcji} other {Usuń # kolekcji}}", + "bulkDeleteFailed": "{count, plural, one {Nie udało się usunąć # kolekcji} few {Nie udało się usunąć # kolekcji} many {Nie udało się usunąć # kolekcji} other {Nie udało się usunąć # kolekcji}}", + "bulkDeleted": "{count, plural, one {Usunięto # kolekcję} few {Usunięto # kolekcje} many {Usunięto # kolekcji} other {Usunięto # kolekcji}}", + "bulkDeleteButton": "Usuń {count, plural, one {# kolekcję} few {# kolekcje} many {# kolekcji} other {# kolekcji}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} zwróconych", "explainDocsExamined": "{n} zbadanych", "explainCollscanHint": "To zapytanie skanuje każdy dokument w kolekcji. Rozważ utworzenie indeksu na filtrowanych polach.", - "explainRawLabel": "Wynik explain" + "explainRawLabel": "Wynik explain", + "docInsertFailed": "Nie udało się wstawić dokumentu", + "docUpdateFailed": "Nie udało się zaktualizować dokumentu", + "indexesLoadFail": "Nie udało się załadować indeksów", + "bulkDeleted": "{count, plural, one {Usunięto # dokument} few {Usunięto # dokumenty} many {Usunięto # dokumentów} other {Usunięto # dokumentów}}", + "bulkDeleteFailed": "Masowe usuwanie nie powiodło się", + "selectedCount": "{count, plural, one {Wybrano # dokument} few {Wybrano # dokumenty} many {Wybrano # dokumentów} other {Wybrano # dokumentów}}", + "deleteSelected": "Usuń zaznaczone", + "clearSelection": "Wyczyść", + "statusLoading": "Ładowanie…", + "statusShowing": "{from}–{to} z {total} dokumentów", + "statusEmpty": "0 dokumentów", + "statusSelected": "Wybrano {count}", + "bulkDeleteTitle": "Usunąć {count, plural, one {# dokument} few {# dokumenty} many {# dokumentów} other {# dokumentów}}?", + "bulkDeleteDescription": "Spowoduje to trwałe usunięcie {count, plural, one {# dokumentu} few {# dokumentów} many {# dokumentów} other {# dokumentów}} z {collection}. Tej operacji nie można cofnąć.", + "bulkDeleting": "Usuwanie…", + "bulkDeleteConfirm": "Usuń wszystkie", + "queryErrorTitle": "Nie udało się załadować dokumentów", + "retry": "Ponów", + "docInsertFailed": "Nie udało się wstawić dokumentu", + "docUpdateFailed": "Nie udało się zaktualizować dokumentu", + "indexesLoadFail": "Nie udało się załadować indeksów", + "bulkDeleted": "{count, plural, one {Usunięto # dokument} few {Usunięto # dokumenty} many {Usunięto # dokumentów} other {Usunięto # dokumentu}}", + "bulkDeleteFailed": "Masowe usuwanie nie powiodło się", + "selectedCount": "{count, plural, one {Zaznaczono # dokument} few {Zaznaczono # dokumenty} many {Zaznaczono # dokumentów} other {Zaznaczono # dokumentu}}", + "deleteSelected": "Usuń zaznaczone", + "clearSelection": "Wyczyść", + "statusLoading": "Ładowanie…", + "statusShowing": "Wyświetlanie {from}–{to} z {total} dokumentów", + "statusEmpty": "0 dokumentów", + "statusSelected": "Zaznaczono: {count}", + "bulkDeleteTitle": "Usunąć {count, plural, one {# dokument} few {# dokumenty} many {# dokumentów} other {# dokumentu}}?", + "bulkDeleteDescription": "Spowoduje to trwałe usunięcie {count, plural, one {# dokumentu} few {# dokumentów} many {# dokumentów} other {# dokumentu}} z {collection}. Tej operacji nie można cofnąć.", + "bulkDeleting": "Usuwanie…", + "bulkDeleteConfirm": "Usuń wszystko", + "queryErrorTitle": "Nie udało się załadować dokumentów", + "retry": "Spróbuj ponownie" }, "tabs": { "filterActive": "Active filter applied", @@ -1733,7 +1775,9 @@ "cancel": "Cancel", "export": "Export", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Eksportuje tylko bieżącą stronę ({count, plural, one {# dokument} few {# dokumenty} many {# dokumentów} other {# dokumentów}})", + "pageOnlyNote": "Eksportuje tylko bieżącą stronę ({count, plural, one {# dokument} few {# dokumenty} many {# dokumentów} other {# dokumentu}})" }, "jsonTree": { "typeLabel": "Type: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Wyniki podglądu ({count})", "previewEmpty": "Brak dokumentów na tym etapie", "previewFail": "Podgląd nie powiódł się" + }, + "indexManager": { + "loading": "Ładowanie indeksów…", + "retry": "Ponów", + "countLabel": "{count, plural, one {# indeks} few {# indeksy} many {# indeksów} other {# indeksów}}", + "totalSize": "łącznie {size}", + "statsDocs": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "statsStorage": "{size} pamięci", + "statsAvgObj": "{size} śr./dok.", + "newIndex": "Nowy indeks", + "createTitle": "Utwórz indeks", + "fieldPlaceholder": "Nazwa pola", + "ascending": "Rosnąco", + "descending": "Malejąco", + "addField": "Dodaj pole", + "unique": "Unikatowy", + "sparse": "Sparse", + "ttlLabel": "TTL (sekundy)", + "ttlPlaceholder": "np. 3600", + "cancel": "Anuluj", + "create": "Utwórz", + "creating": "Tworzenie…", + "created": "Utworzono indeks", + "createFailed": "Nie udało się utworzyć indeksu", + "fieldRequired": "Nazwa pola jest wymagana", + "dropped": "Usunięto indeks „{name}”", + "dropFailed": "Nie udało się usunąć indeksu", + "dropTitle": "Usunąć indeks?", + "dropDescription": "Spowoduje to trwałe usunięcie indeksu „{name}”. Zapytania korzystające z tego indeksu będą wolniejsze.", + "dropping": "Usuwanie…", + "dropConfirm": "Usuń indeks", + "badgeSystem": "systemowy", + "badgeUnique": "unikatowy", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Nie znaleziono indeksów" + }, + "importDialog": { + "title": "Importuj dokumenty do {collection}", + "onlyJson": "Obsługiwane są tylko pliki .json", + "invalidStructure": "Plik musi zawierać tablicę lub obiekt JSON", + "invalidJson": "Nieprawidłowy JSON: nie można przetworzyć pliku", + "imported": "{count, plural, one {Zaimportowano # dokument} few {Zaimportowano # dokumenty} many {Zaimportowano # dokumentów} other {Zaimportowano # dokumentów}}", + "importFailed": "Import nie powiódł się", + "dropHint": "Przeciągnij i upuść lub kliknij, aby przesłać", + "dropSubHint": "Obsługuje tablicę JSON lub NDJSON", + "docsCount": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "previewLabel": "Podgląd (pierwsze 3 dokumenty)", + "moreDocs": "… i {count} więcej dokumentów", + "cancel": "Anuluj", + "importing": "Importowanie…", + "importCount": "{count, plural, one {Importuj # dok.} few {Importuj # dok.} many {Importuj # dok.} other {Importuj # dok.}}", + "importBtn": "Importuj" + }, + "schemaView": { + "analyzing": "Analizowanie schematu…", + "loadFailed": "Nie udało się przeanalizować schematu", + "retry": "Ponów", + "noDocs": "Brak dokumentów do analizy", + "sampled": "Próbkowano {docs} dokumentów · {fields} pól", + "colField": "Pole", + "colTypes": "Typy", + "colCoverage": "Pokrycie" + }, + "indexManager": { + "loading": "Ładowanie indeksów…", + "retry": "Spróbuj ponownie", + "countLabel": "{count, plural, one {# indeks} few {# indeksy} many {# indeksów} other {# indeksu}}", + "totalSize": "{size} łącznie", + "statsDocs": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "statsStorage": "{size} pamięci", + "statsAvgObj": "{size} śr./dok.", + "newIndex": "Nowy indeks", + "createTitle": "Utwórz indeks", + "fieldPlaceholder": "Nazwa pola", + "ascending": "Rosnąco", + "descending": "Malejąco", + "addField": "Dodaj pole", + "unique": "Unikatowy", + "sparse": "Rzadki", + "ttlLabel": "TTL (sekundy)", + "ttlPlaceholder": "np. 3600", + "cancel": "Anuluj", + "create": "Utwórz", + "creating": "Tworzenie…", + "created": "Utworzono indeks", + "createFailed": "Nie udało się utworzyć indeksu", + "fieldRequired": "Nazwa pola jest wymagana", + "dropped": "Usunięto indeks \"{name}\"", + "dropFailed": "Nie udało się usunąć indeksu", + "dropTitle": "Usunąć indeks?", + "dropDescription": "Spowoduje to trwałe usunięcie indeksu \"{name}\". Zapytania korzystające z tego indeksu będą wolniejsze.", + "dropping": "Usuwanie…", + "dropConfirm": "Usuń indeks", + "badgeSystem": "systemowy", + "badgeUnique": "unikatowy", + "badgeSparse": "rzadki", + "badgeTtl": "TTL", + "empty": "Nie znaleziono indeksów" + }, + "importDialog": { + "title": "Importuj dokumenty do {collection}", + "onlyJson": "Obsługiwane są tylko pliki .json", + "invalidStructure": "Plik musi zawierać tablicę lub obiekt JSON", + "invalidJson": "Nieprawidłowy JSON: nie można przeanalizować pliku", + "imported": "{count, plural, one {Zaimportowano # dokument} few {Zaimportowano # dokumenty} many {Zaimportowano # dokumentów} other {Zaimportowano # dokumentu}}", + "importFailed": "Import nie powiódł się", + "dropHint": "Przeciągnij i upuść lub kliknij, aby przesłać", + "dropSubHint": "Obsługuje tablicę JSON lub NDJSON", + "docsCount": "{count, plural, one {# dok.} few {# dok.} many {# dok.} other {# dok.}}", + "previewLabel": "Podgląd (pierwsze 3 dokumenty)", + "moreDocs": "… i {count} więcej dokumentów", + "cancel": "Anuluj", + "importing": "Importowanie…", + "importCount": "{count, plural, one {Importuj # dok.} few {Importuj # dok.} many {Importuj # dok.} other {Importuj # dok.}}", + "importBtn": "Importuj" + }, + "schemaView": { + "analyzing": "Analizowanie schematu…", + "loadFailed": "Nie udało się przeanalizować schematu", + "retry": "Spróbuj ponownie", + "noDocs": "Brak dokumentów do analizy", + "sampled": "Próbkowano {docs} dokumentów · {fields} pól", + "colField": "Pole", + "colTypes": "Typy", + "colCoverage": "Pokrycie" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL pasted and parsed successfully", "responseCopied": "Response copied to clipboard", "codeCopied": "Code copied to clipboard", - "copyFailed": "Kopiowanie do schowka nie powiodło się" + "copyFailed": "Kopiowanie do schowka nie powiodło się", + "curlCopied": "Skopiowano polecenie cURL" }, "layout": { "collections": "Collections", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sending...", "send": "Send", - "invalidJsonBodyHelp": "Nie można wysłać: treść JSON jest nieprawidłowa" + "invalidJsonBodyHelp": "Nie można wysłać: treść JSON jest nieprawidłowa", + "copyCurl": "Kopiuj jako cURL" }, "requestTabs": { "params": "Params", @@ -1915,7 +2087,10 @@ "placeholderName": "My request", "labelFolder": "Folder", "placeholderFolder": "Select a folder", - "save": "Save" + "save": "Save", + "newFolder": "Nowy folder", + "newFolderPlaceholder": "Nazwa folderu", + "create": "Utwórz" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Wykonywanie zapytania…", "emptyTitle": "Uruchom zapytanie, aby zobaczyć wyniki", "emptyHint": "Naciśnij ⌘↩ lub kliknij Uruchom", - "toastNoConnection": "Brak aktywnego połączenia." + "toastNoConnection": "Brak aktywnego połączenia.", + "btnHistory": "Historia", + "btnSaveQuery": "Zapisz zapytanie", + "savedSection": "Zapisane", + "recentSection": "Ostatnie", + "emptyHistory": "Jeszcze nic tu nie ma — wykonaj lub zapisz zapytanie", + "savePlaceholder": "Nazwa zapytania", + "deleteSaved": "Usuń", + "toastQuerySaved": "Zapytanie zapisane" }, "results": { "filterPlaceholder": "Filtruj wyniki…", + "editHint": "Kliknij dwukrotnie komórkę, aby edytować", + "toastRowUpdated": "Wiersz zaktualizowany", + "toastRowsUpdated": "{count, plural, one {# wiersz zaktualizowany} few {# wiersze zaktualizowane} many {# wierszy zaktualizowanych} other {# wiersza zaktualizowanego}}", "rowCount": "{count} {count, plural, one {wiersz} few {wiersze} many {wierszy} other {wierszy}}", "rowCountFiltered": "{filtered} / {total} wierszy", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/pt-BR.json b/apps/desktop-ui/messages/pt-BR.json index d62a017d..00f8e012 100644 --- a/apps/desktop-ui/messages/pt-BR.json +++ b/apps/desktop-ui/messages/pt-BR.json @@ -1580,7 +1580,13 @@ "cancel": "Cancelar", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Falha ao excluir # coleção} other {Falha ao excluir # coleções}}", + "bulkDeleted": "{count, plural, one {# coleção excluída} other {# coleções excluídas}}", + "bulkDeleteButton": "{count, plural, one {Excluir # coleção} other {Excluir # coleções}}", + "bulkDeleteFailed": "{count, plural, one {Falha ao excluir # coleção} other {Falha ao excluir # coleções}}", + "bulkDeleted": "{count, plural, one {# coleção excluída} other {# coleções excluídas}}", + "bulkDeleteButton": "Excluir {count, plural, one {# coleção} other {# coleções}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} retornados", "explainDocsExamined": "{n} examinados", "explainCollscanHint": "Esta consulta varre todos os documentos da coleção. Considere criar um índice nos campos filtrados.", - "explainRawLabel": "Saída do explain" + "explainRawLabel": "Saída do explain", + "docInsertFailed": "Falha ao inserir o documento", + "docUpdateFailed": "Falha ao atualizar o documento", + "indexesLoadFail": "Falha ao carregar os índices", + "bulkDeleted": "{count, plural, one {# documento excluído} other {# documentos excluídos}}", + "bulkDeleteFailed": "Falha na exclusão em massa", + "selectedCount": "{count, plural, one {# documento selecionado} other {# documentos selecionados}}", + "deleteSelected": "Excluir selecionados", + "clearSelection": "Limpar", + "statusLoading": "Carregando…", + "statusShowing": "{from}-{to} de {total} documentos", + "statusEmpty": "0 documentos", + "statusSelected": "{count} selecionados", + "bulkDeleteTitle": "Excluir {count, plural, one {# documento} other {# documentos}}?", + "bulkDeleteDescription": "Isso excluirá permanentemente {count, plural, one {# documento} other {# documentos}} de {collection}. Não é possível desfazer.", + "bulkDeleting": "Excluindo…", + "bulkDeleteConfirm": "Excluir tudo", + "queryErrorTitle": "Falha ao carregar os documentos", + "retry": "Tentar novamente", + "docInsertFailed": "Falha ao inserir o documento", + "docUpdateFailed": "Falha ao atualizar o documento", + "indexesLoadFail": "Falha ao carregar os índices", + "bulkDeleted": "{count, plural, one {# documento excluído} other {# documentos excluídos}}", + "bulkDeleteFailed": "Falha na exclusão em massa", + "selectedCount": "{count, plural, one {# documento selecionado} other {# documentos selecionados}}", + "deleteSelected": "Excluir selecionados", + "clearSelection": "Limpar", + "statusLoading": "Carregando...", + "statusShowing": "Mostrando {from}-{to} de {total} documentos", + "statusEmpty": "0 documentos", + "statusSelected": "{count} selecionados", + "bulkDeleteTitle": "Excluir {count, plural, one {# documento} other {# documentos}}?", + "bulkDeleteDescription": "Isso excluirá permanentemente {count, plural, one {# documento} other {# documentos}} de {collection}. Essa ação não pode ser desfeita.", + "bulkDeleting": "Excluindo...", + "bulkDeleteConfirm": "Excluir tudo", + "queryErrorTitle": "Falha ao carregar os documentos", + "retry": "Tentar novamente" }, "tabs": { "filterActive": "Filtro ativo aplicado", @@ -1733,7 +1775,9 @@ "cancel": "Cancelar", "export": "Exportar", "sheetName": "Dados", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporta apenas a página atual ({count, plural, one {# documento} other {# documentos}})", + "pageOnlyNote": "Exporta apenas a página atual ({count, plural, one {# documento} other {# documentos}})" }, "jsonTree": { "typeLabel": "Tipo: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Resultados da prévia ({count})", "previewEmpty": "Nenhum documento nesta etapa", "previewFail": "Falha na prévia" + }, + "indexManager": { + "loading": "Carregando índices…", + "retry": "Tentar novamente", + "countLabel": "{count, plural, one {# índice} other {# índices}}", + "totalSize": "{size} no total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} de armazenamento", + "statsAvgObj": "{size} méd./doc", + "newIndex": "Novo índice", + "createTitle": "Criar índice", + "fieldPlaceholder": "Nome do campo", + "ascending": "Crescente", + "descending": "Decrescente", + "addField": "Adicionar campo", + "unique": "Único", + "sparse": "Sparse", + "ttlLabel": "TTL (segundos)", + "ttlPlaceholder": "ex.: 3600", + "cancel": "Cancelar", + "create": "Criar", + "creating": "Criando…", + "created": "Índice criado", + "createFailed": "Falha ao criar o índice", + "fieldRequired": "O nome do campo é obrigatório", + "dropped": "Índice \"{name}\" excluído", + "dropFailed": "Falha ao excluir o índice", + "dropTitle": "Excluir índice?", + "dropDescription": "Isso excluirá permanentemente o índice \"{name}\". As consultas que o utilizam ficarão mais lentas.", + "dropping": "Excluindo…", + "dropConfirm": "Excluir índice", + "badgeSystem": "sistema", + "badgeUnique": "único", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Nenhum índice encontrado" + }, + "importDialog": { + "title": "Importar documentos para {collection}", + "onlyJson": "Apenas arquivos .json são suportados", + "invalidStructure": "O arquivo deve conter um array ou objeto JSON", + "invalidJson": "JSON inválido: não foi possível analisar o arquivo", + "imported": "{count, plural, one {# documento importado} other {# documentos importados}}", + "importFailed": "Falha na importação", + "dropHint": "Arraste e solte ou clique para enviar", + "dropSubHint": "Suporta array JSON ou NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Pré-visualização (primeiros 3 documentos)", + "moreDocs": "… e mais {count} documentos", + "cancel": "Cancelar", + "importing": "Importando…", + "importCount": "{count, plural, one {Importar # doc} other {Importar # docs}}", + "importBtn": "Importar" + }, + "schemaView": { + "analyzing": "Analisando o esquema…", + "loadFailed": "Falha ao analisar o esquema", + "retry": "Tentar novamente", + "noDocs": "Nenhum documento para analisar", + "sampled": "{docs} documentos amostrados · {fields} campos", + "colField": "Campo", + "colTypes": "Tipos", + "colCoverage": "Cobertura" + }, + "indexManager": { + "loading": "Carregando índices...", + "retry": "Tentar novamente", + "countLabel": "{count, plural, one {# índice} other {# índices}}", + "totalSize": "{size} no total", + "statsDocs": "{count, plural, one {# doc.} other {# docs.}}", + "statsStorage": "{size} de armazenamento", + "statsAvgObj": "{size} média/doc.", + "newIndex": "Novo índice", + "createTitle": "Criar índice", + "fieldPlaceholder": "Nome do campo", + "ascending": "Crescente", + "descending": "Decrescente", + "addField": "Adicionar campo", + "unique": "Único", + "sparse": "Esparso", + "ttlLabel": "TTL (segundos)", + "ttlPlaceholder": "ex.: 3600", + "cancel": "Cancelar", + "create": "Criar", + "creating": "Criando...", + "created": "Índice criado", + "createFailed": "Falha ao criar o índice", + "fieldRequired": "O nome do campo é obrigatório", + "dropped": "Índice \"{name}\" removido", + "dropFailed": "Falha ao remover o índice", + "dropTitle": "Remover índice?", + "dropDescription": "Isso removerá permanentemente o índice \"{name}\". As consultas que usam esse índice ficarão mais lentas.", + "dropping": "Removendo...", + "dropConfirm": "Remover índice", + "badgeSystem": "sistema", + "badgeUnique": "único", + "badgeSparse": "esparso", + "badgeTtl": "TTL", + "empty": "Nenhum índice encontrado" + }, + "importDialog": { + "title": "Importar documentos para {collection}", + "onlyJson": "Apenas arquivos .json são suportados", + "invalidStructure": "O arquivo deve conter um array ou objeto JSON", + "invalidJson": "JSON inválido: não foi possível analisar o arquivo", + "imported": "{count, plural, one {# documento importado} other {# documentos importados}}", + "importFailed": "Falha na importação", + "dropHint": "Arraste e solte ou clique para enviar", + "dropSubHint": "Suporta array JSON ou NDJSON", + "docsCount": "{count, plural, one {# doc.} other {# docs.}}", + "previewLabel": "Pré-visualização (primeiros 3 documentos)", + "moreDocs": "... e mais {count} documentos", + "cancel": "Cancelar", + "importing": "Importando...", + "importCount": "{count, plural, one {Importar # doc.} other {Importar # docs.}}", + "importBtn": "Importar" + }, + "schemaView": { + "analyzing": "Analisando o esquema...", + "loadFailed": "Falha ao analisar o esquema", + "retry": "Tentar novamente", + "noDocs": "Nenhum documento para analisar", + "sampled": "Amostra de {docs} documentos · {fields} campos", + "colField": "Campo", + "colTypes": "Tipos", + "colCoverage": "Cobertura" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL colado e analisado com sucesso", "responseCopied": "Resposta copiada para a área de transferência", "codeCopied": "Código copiado para a área de transferência", - "copyFailed": "Falha ao copiar para a área de transferência" + "copyFailed": "Falha ao copiar para a área de transferência", + "curlCopied": "Comando cURL copiado" }, "layout": { "collections": "Coleções", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.exemplo.com/v1/...", "sending": "Enviando...", "send": "Enviar", - "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido" + "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido", + "copyCurl": "Copiar como cURL" }, "requestTabs": { "params": "Parâmetros", @@ -1915,7 +2087,10 @@ "placeholderName": "Minha requisição", "labelFolder": "Pasta", "placeholderFolder": "Selecione uma pasta", - "save": "Salvar" + "save": "Salvar", + "newFolder": "Nova pasta", + "newFolderPlaceholder": "Nome da pasta", + "create": "Criar" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Executando consulta…", "emptyTitle": "Execute uma consulta para ver os resultados", "emptyHint": "Pressione ⌘↩ ou clique em Executar", - "toastNoConnection": "Sem conexão ativa." + "toastNoConnection": "Sem conexão ativa.", + "btnHistory": "Histórico", + "btnSaveQuery": "Salvar consulta", + "savedSection": "Salvas", + "recentSection": "Recentes", + "emptyHistory": "Nada ainda — execute ou salve uma consulta", + "savePlaceholder": "Nome da consulta", + "deleteSaved": "Excluir", + "toastQuerySaved": "Consulta salva" }, "results": { "filterPlaceholder": "Filtrar resultados…", + "editHint": "Clique duas vezes em uma célula para editar", + "toastRowUpdated": "Linha atualizada", + "toastRowsUpdated": "{count, plural, one {# linha atualizada} other {# linhas atualizadas}}", "rowCount": "{count} {count, plural, one {linha} other {linhas}}", "rowCountFiltered": "{filtered} / {total} linhas", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/pt.json b/apps/desktop-ui/messages/pt.json index 03c0d08c..8dc4365a 100644 --- a/apps/desktop-ui/messages/pt.json +++ b/apps/desktop-ui/messages/pt.json @@ -1580,7 +1580,13 @@ "cancel": "Cancelar", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Falha ao eliminar # coleção} other {Falha ao eliminar # coleções}}", + "bulkDeleted": "{count, plural, one {# coleção eliminada} other {# coleções eliminadas}}", + "bulkDeleteButton": "{count, plural, one {Eliminar # coleção} other {Eliminar # coleções}}", + "bulkDeleteFailed": "{count, plural, one {Falha ao eliminar # coleção} other {Falha ao eliminar # coleções}}", + "bulkDeleted": "{count, plural, one {# coleção eliminada} other {# coleções eliminadas}}", + "bulkDeleteButton": "Eliminar {count, plural, one {# coleção} other {# coleções}}" }, "document": { "docsBreadcrumb": "{n} docs", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} devolvidos", "explainDocsExamined": "{n} examinados", "explainCollscanHint": "Esta consulta percorre todos os documentos da coleção. Considere criar um índice nos campos filtrados.", - "explainRawLabel": "Saída do explain" + "explainRawLabel": "Saída do explain", + "docInsertFailed": "Falha ao inserir o documento", + "docUpdateFailed": "Falha ao atualizar o documento", + "indexesLoadFail": "Falha ao carregar os índices", + "bulkDeleted": "{count, plural, one {# documento eliminado} other {# documentos eliminados}}", + "bulkDeleteFailed": "Falha na eliminação em massa", + "selectedCount": "{count, plural, one {# documento selecionado} other {# documentos selecionados}}", + "deleteSelected": "Eliminar selecionados", + "clearSelection": "Limpar", + "statusLoading": "A carregar…", + "statusShowing": "{from}-{to} de {total} documentos", + "statusEmpty": "0 documentos", + "statusSelected": "{count} selecionados", + "bulkDeleteTitle": "Eliminar {count, plural, one {# documento} other {# documentos}}?", + "bulkDeleteDescription": "Isto eliminará permanentemente {count, plural, one {# documento} other {# documentos}} de {collection}. Não é possível desfazer.", + "bulkDeleting": "A eliminar…", + "bulkDeleteConfirm": "Eliminar tudo", + "queryErrorTitle": "Falha ao carregar os documentos", + "retry": "Tentar novamente", + "docInsertFailed": "Falha ao inserir o documento", + "docUpdateFailed": "Falha ao atualizar o documento", + "indexesLoadFail": "Falha ao carregar os índices", + "bulkDeleted": "{count, plural, one {# documento eliminado} other {# documentos eliminados}}", + "bulkDeleteFailed": "Falha na eliminação em massa", + "selectedCount": "{count, plural, one {# documento selecionado} other {# documentos selecionados}}", + "deleteSelected": "Eliminar selecionados", + "clearSelection": "Limpar", + "statusLoading": "A carregar…", + "statusShowing": "A mostrar {from}-{to} de {total} documentos", + "statusEmpty": "0 documentos", + "statusSelected": "{count} selecionados", + "bulkDeleteTitle": "Eliminar {count, plural, one {# documento} other {# documentos}}?", + "bulkDeleteDescription": "Isto irá eliminar permanentemente {count, plural, one {# documento} other {# documentos}} de {collection}. Esta ação não pode ser anulada.", + "bulkDeleting": "A eliminar…", + "bulkDeleteConfirm": "Eliminar tudo", + "queryErrorTitle": "Falha ao carregar os documentos", + "retry": "Tentar novamente" }, "tabs": { "filterActive": "Filtro ativo aplicado", @@ -1733,7 +1775,9 @@ "cancel": "Cancelar", "export": "Exportar", "sheetName": "Dados", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporta apenas a página atual ({count, plural, one {# documento} other {# documentos}})", + "pageOnlyNote": "Exporta apenas a página atual ({count, plural, one {# documento} other {# documentos}})" }, "jsonTree": { "typeLabel": "Tipo: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Resultados da pré-visualização ({count})", "previewEmpty": "Sem documentos nesta etapa", "previewFail": "Falha na pré-visualização" + }, + "indexManager": { + "loading": "A carregar índices…", + "retry": "Tentar novamente", + "countLabel": "{count, plural, one {# índice} other {# índices}}", + "totalSize": "{size} no total", + "statsDocs": "{count, plural, one {# doc} other {# docs}}", + "statsStorage": "{size} de armazenamento", + "statsAvgObj": "{size} méd./doc", + "newIndex": "Novo índice", + "createTitle": "Criar índice", + "fieldPlaceholder": "Nome do campo", + "ascending": "Ascendente", + "descending": "Descendente", + "addField": "Adicionar campo", + "unique": "Único", + "sparse": "Sparse", + "ttlLabel": "TTL (segundos)", + "ttlPlaceholder": "ex.: 3600", + "cancel": "Cancelar", + "create": "Criar", + "creating": "A criar…", + "created": "Índice criado", + "createFailed": "Falha ao criar o índice", + "fieldRequired": "O nome do campo é obrigatório", + "dropped": "Índice \"{name}\" eliminado", + "dropFailed": "Falha ao eliminar o índice", + "dropTitle": "Eliminar índice?", + "dropDescription": "Isto eliminará permanentemente o índice \"{name}\". As consultas que o utilizam ficarão mais lentas.", + "dropping": "A eliminar…", + "dropConfirm": "Eliminar índice", + "badgeSystem": "sistema", + "badgeUnique": "único", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Nenhum índice encontrado" + }, + "importDialog": { + "title": "Importar documentos para {collection}", + "onlyJson": "Apenas ficheiros .json são suportados", + "invalidStructure": "O ficheiro deve conter um array ou objeto JSON", + "invalidJson": "JSON inválido: não foi possível analisar o ficheiro", + "imported": "{count, plural, one {# documento importado} other {# documentos importados}}", + "importFailed": "Falha na importação", + "dropHint": "Arraste e largue ou clique para carregar", + "dropSubHint": "Suporta array JSON ou NDJSON", + "docsCount": "{count, plural, one {# doc} other {# docs}}", + "previewLabel": "Pré-visualização (primeiros 3 documentos)", + "moreDocs": "… e mais {count} documentos", + "cancel": "Cancelar", + "importing": "A importar…", + "importCount": "{count, plural, one {Importar # doc} other {Importar # docs}}", + "importBtn": "Importar" + }, + "schemaView": { + "analyzing": "A analisar o esquema…", + "loadFailed": "Falha ao analisar o esquema", + "retry": "Tentar novamente", + "noDocs": "Nenhum documento para analisar", + "sampled": "{docs} documentos amostrados · {fields} campos", + "colField": "Campo", + "colTypes": "Tipos", + "colCoverage": "Cobertura" + }, + "indexManager": { + "loading": "A carregar índices…", + "retry": "Tentar novamente", + "countLabel": "{count, plural, one {# índice} other {# índices}}", + "totalSize": "{size} no total", + "statsDocs": "{count, plural, one {# doc.} other {# docs.}}", + "statsStorage": "{size} de armazenamento", + "statsAvgObj": "{size} média/doc.", + "newIndex": "Novo índice", + "createTitle": "Criar índice", + "fieldPlaceholder": "Nome do campo", + "ascending": "Ascendente", + "descending": "Descendente", + "addField": "Adicionar campo", + "unique": "Único", + "sparse": "Esparso", + "ttlLabel": "TTL (segundos)", + "ttlPlaceholder": "ex.: 3600", + "cancel": "Cancelar", + "create": "Criar", + "creating": "A criar…", + "created": "Índice criado", + "createFailed": "Falha ao criar o índice", + "fieldRequired": "O nome do campo é obrigatório", + "dropped": "Índice \"{name}\" removido", + "dropFailed": "Falha ao remover o índice", + "dropTitle": "Remover índice?", + "dropDescription": "Isto irá remover permanentemente o índice \"{name}\". As consultas que usam este índice ficarão mais lentas.", + "dropping": "A remover…", + "dropConfirm": "Remover índice", + "badgeSystem": "sistema", + "badgeUnique": "único", + "badgeSparse": "esparso", + "badgeTtl": "TTL", + "empty": "Nenhum índice encontrado" + }, + "importDialog": { + "title": "Importar documentos para {collection}", + "onlyJson": "Apenas são suportados ficheiros .json", + "invalidStructure": "O ficheiro tem de conter um array ou objeto JSON", + "invalidJson": "JSON inválido: não foi possível analisar o ficheiro", + "imported": "{count, plural, one {# documento importado} other {# documentos importados}}", + "importFailed": "Falha na importação", + "dropHint": "Arraste e largue ou clique para carregar", + "dropSubHint": "Suporta array JSON ou NDJSON", + "docsCount": "{count, plural, one {# doc.} other {# docs.}}", + "previewLabel": "Pré-visualização (primeiros 3 documentos)", + "moreDocs": "… e mais {count} documentos", + "cancel": "Cancelar", + "importing": "A importar…", + "importCount": "{count, plural, one {Importar # doc.} other {Importar # docs.}}", + "importBtn": "Importar" + }, + "schemaView": { + "analyzing": "A analisar o esquema…", + "loadFailed": "Falha ao analisar o esquema", + "retry": "Tentar novamente", + "noDocs": "Nenhum documento para analisar", + "sampled": "Amostra de {docs} documentos · {fields} campos", + "colField": "Campo", + "colTypes": "Tipos", + "colCoverage": "Cobertura" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL colado e analisado com sucesso", "responseCopied": "Resposta copiada para a área de transferência", "codeCopied": "Código copiado para a área de transferência", - "copyFailed": "Falha ao copiar para a área de transferência" + "copyFailed": "Falha ao copiar para a área de transferência", + "curlCopied": "Comando cURL copiado" }, "layout": { "collections": "Coleções", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.exemplo.com/v1/...", "sending": "A enviar...", "send": "Enviar", - "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido" + "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido", + "copyCurl": "Copiar como cURL" }, "requestTabs": { "params": "Parâmetros", @@ -1915,7 +2087,10 @@ "placeholderName": "O meu pedido", "labelFolder": "Pasta", "placeholderFolder": "Seleciona uma pasta", - "save": "Guardar" + "save": "Guardar", + "newFolder": "Nova pasta", + "newFolderPlaceholder": "Nome da pasta", + "create": "Criar" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Executando consulta…", "emptyTitle": "Execute uma consulta para ver os resultados", "emptyHint": "Pressione ⌘↩ ou clique em Executar", - "toastNoConnection": "Sem conexão ativa." + "toastNoConnection": "Sem conexão ativa.", + "btnHistory": "Histórico", + "btnSaveQuery": "Guardar consulta", + "savedSection": "Guardadas", + "recentSection": "Recentes", + "emptyHistory": "Ainda nada — execute ou guarde uma consulta", + "savePlaceholder": "Nome da consulta", + "deleteSaved": "Eliminar", + "toastQuerySaved": "Consulta guardada" }, "results": { "filterPlaceholder": "Filtrar resultados…", + "editHint": "Faça duplo clique numa célula para editar", + "toastRowUpdated": "Linha atualizada", + "toastRowsUpdated": "{count, plural, one {# linha atualizada} other {# linhas atualizadas}}", "rowCount": "{count} {count, plural, one {linha} other {linhas}}", "rowCountFiltered": "{filtered} / {total} linhas", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/ru.json b/apps/desktop-ui/messages/ru.json index b3ac90e4..82592912 100644 --- a/apps/desktop-ui/messages/ru.json +++ b/apps/desktop-ui/messages/ru.json @@ -1580,7 +1580,13 @@ "cancel": "Отмена", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Не удалось удалить # коллекцию} few {Не удалось удалить # коллекции} many {Не удалось удалить # коллекций} other {Не удалось удалить # коллекций}}", + "bulkDeleted": "{count, plural, one {Удалена # коллекция} few {Удалено # коллекции} many {Удалено # коллекций} other {Удалено # коллекций}}", + "bulkDeleteButton": "{count, plural, one {Удалить # коллекцию} few {Удалить # коллекции} many {Удалить # коллекций} other {Удалить # коллекций}}", + "bulkDeleteFailed": "{count, plural, one {Не удалось удалить # коллекцию} few {Не удалось удалить # коллекции} other {Не удалось удалить # коллекций}}", + "bulkDeleted": "{count, plural, one {Удалена # коллекция} few {Удалено # коллекции} other {Удалено # коллекций}}", + "bulkDeleteButton": "Удалить {count, plural, one {# коллекцию} few {# коллекции} other {# коллекций}}" }, "document": { "docsBreadcrumb": "{n} док.", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} возвращено", "explainDocsExamined": "{n} проверено", "explainCollscanHint": "Этот запрос сканирует каждый документ коллекции. Рассмотрите создание индекса по фильтруемым полям.", - "explainRawLabel": "Вывод explain" + "explainRawLabel": "Вывод explain", + "docInsertFailed": "Не удалось вставить документ", + "docUpdateFailed": "Не удалось обновить документ", + "indexesLoadFail": "Не удалось загрузить индексы", + "bulkDeleted": "{count, plural, one {Удалён # документ} few {Удалено # документа} many {Удалено # документов} other {Удалено # документов}}", + "bulkDeleteFailed": "Массовое удаление не выполнено", + "selectedCount": "{count, plural, one {Выбран # документ} few {Выбрано # документа} many {Выбрано # документов} other {Выбрано # документов}}", + "deleteSelected": "Удалить выбранные", + "clearSelection": "Очистить", + "statusLoading": "Загрузка…", + "statusShowing": "{from}–{to} из {total} документов", + "statusEmpty": "0 документов", + "statusSelected": "Выбрано {count}", + "bulkDeleteTitle": "Удалить {count, plural, one {# документ} few {# документа} many {# документов} other {# документов}}?", + "bulkDeleteDescription": "Это навсегда удалит {count, plural, one {# документ} few {# документа} many {# документов} other {# документов}} из {collection}. Это действие нельзя отменить.", + "bulkDeleting": "Удаление…", + "bulkDeleteConfirm": "Удалить все", + "queryErrorTitle": "Не удалось загрузить документы", + "retry": "Повторить", + "docInsertFailed": "Не удалось вставить документ", + "docUpdateFailed": "Не удалось обновить документ", + "indexesLoadFail": "Не удалось загрузить индексы", + "bulkDeleted": "{count, plural, one {Удалён # документ} few {Удалено # документа} other {Удалено # документов}}", + "bulkDeleteFailed": "Массовое удаление не удалось", + "selectedCount": "{count, plural, one {Выбран # документ} few {Выбрано # документа} other {Выбрано # документов}}", + "deleteSelected": "Удалить выбранные", + "clearSelection": "Очистить", + "statusLoading": "Загрузка…", + "statusShowing": "Показано {from}–{to} из {total} документов", + "statusEmpty": "0 документов", + "statusSelected": "Выбрано: {count}", + "bulkDeleteTitle": "Удалить {count, plural, one {# документ} few {# документа} other {# документов}}?", + "bulkDeleteDescription": "Это навсегда удалит {count, plural, one {# документ} few {# документа} other {# документов}} из {collection}. Это действие нельзя отменить.", + "bulkDeleting": "Удаление…", + "bulkDeleteConfirm": "Удалить все", + "queryErrorTitle": "Не удалось загрузить документы", + "retry": "Повторить" }, "tabs": { "filterActive": "Активный фильтр применён", @@ -1733,7 +1775,9 @@ "cancel": "Отмена", "export": "Экспорт", "sheetName": "Данные", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Экспортирует только текущую страницу ({count, plural, one {# документ} few {# документа} many {# документов} other {# документов}})", + "pageOnlyNote": "Экспортирует только текущую страницу ({count, plural, one {# документ} few {# документа} other {# документов}})" }, "jsonTree": { "typeLabel": "Тип: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Результаты предпросмотра ({count})", "previewEmpty": "На этом этапе нет документов", "previewFail": "Ошибка предпросмотра" + }, + "indexManager": { + "loading": "Загрузка индексов…", + "retry": "Повторить", + "countLabel": "{count, plural, one {# индекс} few {# индекса} many {# индексов} other {# индексов}}", + "totalSize": "всего {size}", + "statsDocs": "{count, plural, one {# док.} few {# док.} many {# док.} other {# док.}}", + "statsStorage": "{size} хранилище", + "statsAvgObj": "{size} сред./док.", + "newIndex": "Новый индекс", + "createTitle": "Создать индекс", + "fieldPlaceholder": "Имя поля", + "ascending": "По возрастанию", + "descending": "По убыванию", + "addField": "Добавить поле", + "unique": "Уникальный", + "sparse": "Sparse", + "ttlLabel": "TTL (секунды)", + "ttlPlaceholder": "напр. 3600", + "cancel": "Отмена", + "create": "Создать", + "creating": "Создание…", + "created": "Индекс создан", + "createFailed": "Не удалось создать индекс", + "fieldRequired": "Имя поля обязательно", + "dropped": "Индекс «{name}» удалён", + "dropFailed": "Не удалось удалить индекс", + "dropTitle": "Удалить индекс?", + "dropDescription": "Это навсегда удалит индекс «{name}». Запросы, использующие этот индекс, станут медленнее.", + "dropping": "Удаление…", + "dropConfirm": "Удалить индекс", + "badgeSystem": "системный", + "badgeUnique": "уникальный", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Индексы не найдены" + }, + "importDialog": { + "title": "Импорт документов в {collection}", + "onlyJson": "Поддерживаются только файлы .json", + "invalidStructure": "Файл должен содержать массив или объект JSON", + "invalidJson": "Недопустимый JSON: не удалось разобрать файл", + "imported": "{count, plural, one {Импортирован # документ} few {Импортировано # документа} many {Импортировано # документов} other {Импортировано # документов}}", + "importFailed": "Не удалось выполнить импорт", + "dropHint": "Перетащите или нажмите для загрузки", + "dropSubHint": "Поддерживает массив JSON или NDJSON", + "docsCount": "{count, plural, one {# док.} few {# док.} many {# док.} other {# док.}}", + "previewLabel": "Предпросмотр (первые 3 документа)", + "moreDocs": "… и ещё {count} документов", + "cancel": "Отмена", + "importing": "Импорт…", + "importCount": "{count, plural, one {Импортировать # док.} few {Импортировать # док.} many {Импортировать # док.} other {Импортировать # док.}}", + "importBtn": "Импорт" + }, + "schemaView": { + "analyzing": "Анализ схемы…", + "loadFailed": "Не удалось проанализировать схему", + "retry": "Повторить", + "noDocs": "Нет документов для анализа", + "sampled": "Выборка {docs} документов · {fields} полей", + "colField": "Поле", + "colTypes": "Типы", + "colCoverage": "Покрытие" + }, + "indexManager": { + "loading": "Загрузка индексов…", + "retry": "Повторить", + "countLabel": "{count, plural, one {# индекс} few {# индекса} other {# индексов}}", + "totalSize": "{size} всего", + "statsDocs": "{count, plural, one {# док.} few {# док.} other {# док.}}", + "statsStorage": "{size} хранилища", + "statsAvgObj": "{size} сред./док.", + "newIndex": "Новый индекс", + "createTitle": "Создать индекс", + "fieldPlaceholder": "Имя поля", + "ascending": "По возрастанию", + "descending": "По убыванию", + "addField": "Добавить поле", + "unique": "Уникальный", + "sparse": "Разреженный", + "ttlLabel": "TTL (секунды)", + "ttlPlaceholder": "напр. 3600", + "cancel": "Отмена", + "create": "Создать", + "creating": "Создание…", + "created": "Индекс создан", + "createFailed": "Не удалось создать индекс", + "fieldRequired": "Имя поля обязательно", + "dropped": "Индекс \"{name}\" удалён", + "dropFailed": "Не удалось удалить индекс", + "dropTitle": "Удалить индекс?", + "dropDescription": "Это навсегда удалит индекс \"{name}\". Запросы, использующие этот индекс, замедлятся.", + "dropping": "Удаление…", + "dropConfirm": "Удалить индекс", + "badgeSystem": "система", + "badgeUnique": "уникальный", + "badgeSparse": "разреженный", + "badgeTtl": "TTL", + "empty": "Индексы не найдены" + }, + "importDialog": { + "title": "Импорт документов в {collection}", + "onlyJson": "Поддерживаются только файлы .json", + "invalidStructure": "Файл должен содержать массив или объект JSON", + "invalidJson": "Недопустимый JSON: не удалось разобрать файл", + "imported": "{count, plural, one {Импортирован # документ} few {Импортировано # документа} other {Импортировано # документов}}", + "importFailed": "Ошибка импорта", + "dropHint": "Перетащите или нажмите, чтобы загрузить", + "dropSubHint": "Поддерживает массив JSON или NDJSON", + "docsCount": "{count, plural, one {# док.} few {# док.} other {# док.}}", + "previewLabel": "Предпросмотр (первые 3 документа)", + "moreDocs": "… и ещё {count} документов", + "cancel": "Отмена", + "importing": "Импорт…", + "importCount": "{count, plural, one {Импорт # док.} few {Импорт # док.} other {Импорт # док.}}", + "importBtn": "Импорт" + }, + "schemaView": { + "analyzing": "Анализ схемы…", + "loadFailed": "Не удалось проанализировать схему", + "retry": "Повторить", + "noDocs": "Нет документов для анализа", + "sampled": "Выборка из {docs} документов · {fields} полей", + "colField": "Поле", + "colTypes": "Типы", + "colCoverage": "Покрытие" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL вставлен и успешно разобран", "responseCopied": "Ответ скопирован в буфер обмена", "codeCopied": "Код скопирован в буфер обмена", - "copyFailed": "Не удалось скопировать в буфер обмена" + "copyFailed": "Не удалось скопировать в буфер обмена", + "curlCopied": "Команда cURL скопирована" }, "layout": { "collections": "Коллекции", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Отправка...", "send": "Отправить", - "invalidJsonBodyHelp": "Невозможно отправить: тело JSON недействительно" + "invalidJsonBodyHelp": "Невозможно отправить: тело JSON недействительно", + "copyCurl": "Копировать как cURL" }, "requestTabs": { "params": "Параметры", @@ -1915,7 +2087,10 @@ "placeholderName": "Мой запрос", "labelFolder": "Папка", "placeholderFolder": "Выберите папку", - "save": "Сохранить" + "save": "Сохранить", + "newFolder": "Новая папка", + "newFolderPlaceholder": "Имя папки", + "create": "Создать" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Выполнение запроса…", "emptyTitle": "Выполните запрос для просмотра результатов", "emptyHint": "Нажмите ⌘↩ или кнопку Выполнить", - "toastNoConnection": "Нет активного подключения." + "toastNoConnection": "Нет активного подключения.", + "btnHistory": "История", + "btnSaveQuery": "Сохранить запрос", + "savedSection": "Сохранённые", + "recentSection": "Недавние", + "emptyHistory": "Пока пусто — выполните или сохраните запрос", + "savePlaceholder": "Название запроса", + "deleteSaved": "Удалить", + "toastQuerySaved": "Запрос сохранён" }, "results": { "filterPlaceholder": "Фильтр результатов…", + "editHint": "Дважды щёлкните ячейку для редактирования", + "toastRowUpdated": "Строка обновлена", + "toastRowsUpdated": "{count, plural, one {Обновлена # строка} few {Обновлены # строки} many {Обновлено # строк} other {Обновлено # строки}}", "rowCount": "{count} {count, plural, one {строка} few {строки} other {строк}}", "rowCountFiltered": "{filtered} / {total} строк", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/sv.json b/apps/desktop-ui/messages/sv.json index d07ce6bf..badc30d6 100644 --- a/apps/desktop-ui/messages/sv.json +++ b/apps/desktop-ui/messages/sv.json @@ -1580,7 +1580,10 @@ "cancel": "Avbryt", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Misslyckades att radera # collection} other {Misslyckades att radera # collections}}", + "bulkDeleted": "{count, plural, one {Raderade # collection} other {Raderade # collections}}", + "bulkDeleteButton": "Radera {count, plural, one {# collection} other {# collections}}" }, "document": { "docsBreadcrumb": "{n} dok", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} returnerade", "explainDocsExamined": "{n} granskade", "explainCollscanHint": "Denna fråga skannar varje dokument i collectionen. Överväg att skapa ett index på de filtrerade fälten.", - "explainRawLabel": "Explain-utdata" + "explainRawLabel": "Explain-utdata", + "docInsertFailed": "Misslyckades att infoga dokument", + "docUpdateFailed": "Misslyckades att uppdatera dokument", + "indexesLoadFail": "Misslyckades att läsa in index", + "bulkDeleted": "{count, plural, one {# dokument raderat} other {# dokument raderade}}", + "bulkDeleteFailed": "Massradering misslyckades", + "selectedCount": "{count, plural, one {# dokument valt} other {# dokument valda}}", + "deleteSelected": "Radera valda", + "clearSelection": "Rensa", + "statusLoading": "Läser in...", + "statusShowing": "Visar {from}–{to} av {total} dokument", + "statusEmpty": "0 dokument", + "statusSelected": "{count} valda", + "bulkDeleteTitle": "Radera {count, plural, one {# dokument} other {# dokument}}?", + "bulkDeleteDescription": "Detta raderar permanent {count, plural, one {# dokument} other {# dokument}} från {collection}. Detta kan inte ångras.", + "bulkDeleting": "Raderar...", + "bulkDeleteConfirm": "Radera alla", + "queryErrorTitle": "Misslyckades att läsa in dokument", + "retry": "Försök igen" }, "tabs": { "filterActive": "Aktivt filter tillämpat", @@ -1733,7 +1754,8 @@ "cancel": "Avbryt", "export": "Exportera", "sheetName": "Data", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Exporterar endast aktuell sida ({count, plural, one {# dokument} other {# dokument}})" }, "jsonTree": { "typeLabel": "Typ: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Förhandsgranskningsresultat ({count})", "previewEmpty": "Inga dokument i detta steg", "previewFail": "Förhandsgranskning misslyckades" + }, + "indexManager": { + "loading": "Läser in index...", + "retry": "Försök igen", + "countLabel": "{count, plural, one {# index} other {# index}}", + "totalSize": "{size} totalt", + "statsDocs": "{count, plural, one {# dok} other {# dok}}", + "statsStorage": "{size} lagring", + "statsAvgObj": "{size} snitt/dok", + "newIndex": "Nytt index", + "createTitle": "Skapa index", + "fieldPlaceholder": "Fältnamn", + "ascending": "Stigande", + "descending": "Fallande", + "addField": "Lägg till fält", + "unique": "Unik", + "sparse": "Sparse", + "ttlLabel": "TTL (sekunder)", + "ttlPlaceholder": "t.ex. 3600", + "cancel": "Avbryt", + "create": "Skapa", + "creating": "Skapar...", + "created": "Index skapat", + "createFailed": "Misslyckades att skapa index", + "fieldRequired": "Fältnamn krävs", + "dropped": "Index \"{name}\" borttaget", + "dropFailed": "Misslyckades att ta bort index", + "dropTitle": "Ta bort index?", + "dropDescription": "Detta tar permanent bort indexet \"{name}\". Frågor som använder detta index blir långsammare.", + "dropping": "Tar bort...", + "dropConfirm": "Ta bort index", + "badgeSystem": "system", + "badgeUnique": "unik", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Inga index hittades" + }, + "importDialog": { + "title": "Importera dokument till {collection}", + "onlyJson": "Endast .json-filer stöds", + "invalidStructure": "Filen måste innehålla en JSON-array eller ett objekt", + "invalidJson": "Ogiltig JSON: kunde inte tolka filen", + "imported": "{count, plural, one {Importerade # dokument} other {Importerade # dokument}}", + "importFailed": "Import misslyckades", + "dropHint": "Dra och släpp eller klicka för att ladda upp", + "dropSubHint": "Stöder JSON-array eller NDJSON", + "docsCount": "{count, plural, one {# dok} other {# dok}}", + "previewLabel": "Förhandsgranskning (första 3 dokumenten)", + "moreDocs": "... och {count} dokument till", + "cancel": "Avbryt", + "importing": "Importerar...", + "importCount": "{count, plural, one {Importera # dok} other {Importera # dok}}", + "importBtn": "Importera" + }, + "schemaView": { + "analyzing": "Analyserar schema...", + "loadFailed": "Misslyckades att analysera schema", + "retry": "Försök igen", + "noDocs": "Inga dokument att analysera", + "sampled": "Samplade {docs} dokument · {fields} fält", + "colField": "Fält", + "colTypes": "Typer", + "colCoverage": "Täckning" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL inklistrad och tolkad", "responseCopied": "Svar kopierat till urklipp", "codeCopied": "Kod kopierad till urklipp", - "copyFailed": "Kopiering till urklipp misslyckades" + "copyFailed": "Kopiering till urklipp misslyckades", + "curlCopied": "cURL-kommando kopierat" }, "layout": { "collections": "Samlingar", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Skickar...", "send": "Skicka", - "invalidJsonBodyHelp": "Kan inte skicka: JSON-kroppen är ogiltig" + "invalidJsonBodyHelp": "Kan inte skicka: JSON-kroppen är ogiltig", + "copyCurl": "Kopiera som cURL" }, "requestTabs": { "params": "Parametrar", @@ -1915,7 +2002,10 @@ "placeholderName": "Min förfrågan", "labelFolder": "Mapp", "placeholderFolder": "Välj en mapp", - "save": "Spara" + "save": "Spara", + "newFolder": "Ny mapp", + "newFolderPlaceholder": "Mappnamn", + "create": "Skapa" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Kör fråga…", "emptyTitle": "Kör en fråga för att se resultat", "emptyHint": "Tryck ⌘↩ eller klicka Kör", - "toastNoConnection": "Ingen aktiv anslutning." + "toastNoConnection": "Ingen aktiv anslutning.", + "btnHistory": "Historik", + "btnSaveQuery": "Spara fråga", + "savedSection": "Sparade", + "recentSection": "Senaste", + "emptyHistory": "Inget här ännu — kör eller spara en fråga", + "savePlaceholder": "Frågenamn", + "deleteSaved": "Ta bort", + "toastQuerySaved": "Frågan sparad" }, "results": { "filterPlaceholder": "Filtrera resultat…", + "editHint": "Dubbelklicka på en cell för att redigera", + "toastRowUpdated": "Rad uppdaterad", + "toastRowsUpdated": "{count, plural, one {# rad uppdaterad} other {# rader uppdaterade}}", "rowCount": "{count} {count, plural, one {rad} other {rader}}", "rowCountFiltered": "{filtered} / {total} rader", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/tr.json b/apps/desktop-ui/messages/tr.json index 68c2d6d6..eefca454 100644 --- a/apps/desktop-ui/messages/tr.json +++ b/apps/desktop-ui/messages/tr.json @@ -1580,7 +1580,10 @@ "cancel": "İptal", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {# koleksiyon silinemedi} other {# koleksiyon silinemedi}}", + "bulkDeleted": "{count, plural, one {# koleksiyon silindi} other {# koleksiyon silindi}}", + "bulkDeleteButton": "{count, plural, one {# Koleksiyonu Sil} other {# Koleksiyonu Sil}}" }, "document": { "docsBreadcrumb": "{n} belge", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} döndürüldü", "explainDocsExamined": "{n} incelendi", "explainCollscanHint": "Bu sorgu koleksiyondaki her belgeyi tarıyor. Filtrelenen alanlarda bir dizin oluşturmayı düşünün.", - "explainRawLabel": "Explain çıktısı" + "explainRawLabel": "Explain çıktısı", + "docInsertFailed": "Belge eklenemedi", + "docUpdateFailed": "Belge güncellenemedi", + "indexesLoadFail": "Dizinler yüklenemedi", + "bulkDeleted": "{count, plural, one {# belge silindi} other {# belge silindi}}", + "bulkDeleteFailed": "Toplu silme başarısız", + "selectedCount": "{count, plural, one {# belge seçildi} other {# belge seçildi}}", + "deleteSelected": "Seçilenleri Sil", + "clearSelection": "Temizle", + "statusLoading": "Yükleniyor...", + "statusShowing": "{total} belgeden {from}–{to} arası gösteriliyor", + "statusEmpty": "0 belge", + "statusSelected": "{count} seçili", + "bulkDeleteTitle": "{count, plural, one {# belge} other {# belge}} silinsin mi?", + "bulkDeleteDescription": "Bu işlem {collection} koleksiyonundan {count, plural, one {# belgeyi} other {# belgeyi}} kalıcı olarak silecek. Bu geri alınamaz.", + "bulkDeleting": "Siliniyor...", + "bulkDeleteConfirm": "Tümünü Sil", + "queryErrorTitle": "Belgeler yüklenemedi", + "retry": "Yeniden dene" }, "tabs": { "filterActive": "Aktif filtre uygulandı", @@ -1733,7 +1754,8 @@ "cancel": "İptal", "export": "Dışa Aktar", "sheetName": "Veri", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Yalnızca geçerli sayfayı dışa aktarır ({count, plural, one {# belge} other {# belge}})" }, "jsonTree": { "typeLabel": "Tür: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Önizleme sonuçları ({count})", "previewEmpty": "Bu aşamada belge yok", "previewFail": "Önizleme başarısız" + }, + "indexManager": { + "loading": "Dizinler yükleniyor...", + "retry": "Yeniden dene", + "countLabel": "{count, plural, one {# dizin} other {# dizin}}", + "totalSize": "toplam {size}", + "statsDocs": "{count, plural, one {# belge} other {# belge}}", + "statsStorage": "{size} depolama", + "statsAvgObj": "{size} ort/belge", + "newIndex": "Yeni Dizin", + "createTitle": "Dizin Oluştur", + "fieldPlaceholder": "Alan adı", + "ascending": "Artan", + "descending": "Azalan", + "addField": "Alan ekle", + "unique": "Benzersiz", + "sparse": "Sparse", + "ttlLabel": "TTL (saniye)", + "ttlPlaceholder": "örn. 3600", + "cancel": "İptal", + "create": "Oluştur", + "creating": "Oluşturuluyor...", + "created": "Dizin oluşturuldu", + "createFailed": "Dizin oluşturulamadı", + "fieldRequired": "Alan adı gerekli", + "dropped": "\"{name}\" dizini silindi", + "dropFailed": "Dizin silinemedi", + "dropTitle": "Dizin silinsin mi?", + "dropDescription": "Bu işlem \"{name}\" dizinini kalıcı olarak silecek. Bu dizini kullanan sorgular yavaşlayacak.", + "dropping": "Siliniyor...", + "dropConfirm": "Dizini Sil", + "badgeSystem": "sistem", + "badgeUnique": "benzersiz", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Dizin bulunamadı" + }, + "importDialog": { + "title": "Belgeleri {collection} koleksiyonuna aktar", + "onlyJson": "Yalnızca .json dosyaları desteklenir", + "invalidStructure": "Dosya bir JSON dizisi veya nesnesi içermelidir", + "invalidJson": "Geçersiz JSON: dosya ayrıştırılamadı", + "imported": "{count, plural, one {# belge içe aktarıldı} other {# belge içe aktarıldı}}", + "importFailed": "İçe aktarma başarısız", + "dropHint": "Sürükleyip bırakın veya yüklemek için tıklayın", + "dropSubHint": "JSON dizisi veya NDJSON destekler", + "docsCount": "{count, plural, one {# belge} other {# belge}}", + "previewLabel": "Önizleme (ilk 3 belge)", + "moreDocs": "... ve {count} belge daha", + "cancel": "İptal", + "importing": "İçe aktarılıyor...", + "importCount": "{count, plural, one {# Belgeyi Aktar} other {# Belgeyi Aktar}}", + "importBtn": "İçe Aktar" + }, + "schemaView": { + "analyzing": "Şema analiz ediliyor...", + "loadFailed": "Şema analiz edilemedi", + "retry": "Yeniden dene", + "noDocs": "Analiz edilecek belge yok", + "sampled": "{docs} belge örneklendi · {fields} alan", + "colField": "Alan", + "colTypes": "Türler", + "colCoverage": "Kapsam" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL yapıştırıldı ve başarıyla ayrıştırıldı", "responseCopied": "Yanıt panoya kopyalandı", "codeCopied": "Kod panoya kopyalandı", - "copyFailed": "Panoya kopyalanamadı" + "copyFailed": "Panoya kopyalanamadı", + "curlCopied": "cURL komutu kopyalandı" }, "layout": { "collections": "Koleksiyonlar", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.ornek.com/v1/...", "sending": "Gönderiliyor...", "send": "Gönder", - "invalidJsonBodyHelp": "Gönderilemedi: JSON gövdesi geçersiz" + "invalidJsonBodyHelp": "Gönderilemedi: JSON gövdesi geçersiz", + "copyCurl": "cURL olarak kopyala" }, "requestTabs": { "params": "Parametreler", @@ -1915,7 +2002,10 @@ "placeholderName": "İsteğim", "labelFolder": "Klasör", "placeholderFolder": "Bir klasör seçin", - "save": "Kaydet" + "save": "Kaydet", + "newFolder": "Yeni klasör", + "newFolderPlaceholder": "Klasör adı", + "create": "Oluştur" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Sorgu çalıştırılıyor…", "emptyTitle": "Sonuçları görmek için sorgu çalıştırın", "emptyHint": "⌘↩ basın veya Çalıştır'a tıklayın", - "toastNoConnection": "Aktif bağlantı yok." + "toastNoConnection": "Aktif bağlantı yok.", + "btnHistory": "Geçmiş", + "btnSaveQuery": "Sorguyu kaydet", + "savedSection": "Kayıtlı", + "recentSection": "Son", + "emptyHistory": "Henüz bir şey yok — bir sorgu çalıştırın veya kaydedin", + "savePlaceholder": "Sorgu adı", + "deleteSaved": "Sil", + "toastQuerySaved": "Sorgu kaydedildi" }, "results": { "filterPlaceholder": "Sonuçları filtrele…", + "editHint": "Düzenlemek için hücreye çift tıklayın", + "toastRowUpdated": "Satır güncellendi", + "toastRowsUpdated": "{count, plural, one {# satır güncellendi} other {# satır güncellendi}}", "rowCount": "{count} satır", "rowCountFiltered": "{filtered} / {total} satır", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/uk.json b/apps/desktop-ui/messages/uk.json index 427fb237..51c34f08 100644 --- a/apps/desktop-ui/messages/uk.json +++ b/apps/desktop-ui/messages/uk.json @@ -1580,7 +1580,10 @@ "cancel": "Скасувати", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Не вдалося видалити # колекцію} few {Не вдалося видалити # колекції} other {Не вдалося видалити # колекцій}}", + "bulkDeleted": "{count, plural, one {Видалено # колекцію} few {Видалено # колекції} other {Видалено # колекцій}}", + "bulkDeleteButton": "Видалити {count, plural, one {# колекцію} few {# колекції} other {# колекцій}}" }, "document": { "docsBreadcrumb": "{n} документів", @@ -1657,7 +1660,25 @@ "explainReturned": "{n} повернуто", "explainDocsExamined": "{n} перевірено", "explainCollscanHint": "Цей запит сканує кожен документ колекції. Розгляньте створення індексу для фільтрованих полів.", - "explainRawLabel": "Вивід explain" + "explainRawLabel": "Вивід explain", + "docInsertFailed": "Не вдалося вставити документ", + "docUpdateFailed": "Не вдалося оновити документ", + "indexesLoadFail": "Не вдалося завантажити індекси", + "bulkDeleted": "{count, plural, one {# документ видалено} few {# документи видалено} other {# документів видалено}}", + "bulkDeleteFailed": "Масове видалення не вдалося", + "selectedCount": "{count, plural, one {# документ обрано} few {# документи обрано} other {# документів обрано}}", + "deleteSelected": "Видалити обрані", + "clearSelection": "Очистити", + "statusLoading": "Завантаження...", + "statusShowing": "Показано {from}–{to} з {total} документів", + "statusEmpty": "0 документів", + "statusSelected": "{count} обрано", + "bulkDeleteTitle": "Видалити {count, plural, one {# документ} few {# документи} other {# документів}}?", + "bulkDeleteDescription": "Це назавжди видалить {count, plural, one {# документ} few {# документи} other {# документів}} з {collection}. Цю дію не можна скасувати.", + "bulkDeleting": "Видалення...", + "bulkDeleteConfirm": "Видалити все", + "queryErrorTitle": "Не вдалося завантажити документи", + "retry": "Повторити" }, "tabs": { "filterActive": "Активний фільтр застосовано", @@ -1733,7 +1754,8 @@ "cancel": "Скасувати", "export": "Експорт", "sheetName": "Дані", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Експортує лише поточну сторінку ({count, plural, one {# документ} few {# документи} other {# документів}})" }, "jsonTree": { "typeLabel": "Тип: {type}", @@ -1748,6 +1770,69 @@ "previewResult": "Результати перегляду ({count})", "previewEmpty": "На цьому етапі немає документів", "previewFail": "Помилка попереднього перегляду" + }, + "indexManager": { + "loading": "Завантаження індексів...", + "retry": "Повторити", + "countLabel": "{count, plural, one {# індекс} few {# індекси} other {# індексів}}", + "totalSize": "{size} загалом", + "statsDocs": "{count, plural, one {# док} few {# док} other {# док}}", + "statsStorage": "{size} сховище", + "statsAvgObj": "{size} серед./док", + "newIndex": "Новий індекс", + "createTitle": "Створити індекс", + "fieldPlaceholder": "Назва поля", + "ascending": "За зростанням", + "descending": "За спаданням", + "addField": "Додати поле", + "unique": "Унікальний", + "sparse": "Розріджений", + "ttlLabel": "TTL (секунди)", + "ttlPlaceholder": "напр. 3600", + "cancel": "Скасувати", + "create": "Створити", + "creating": "Створення...", + "created": "Індекс створено", + "createFailed": "Не вдалося створити індекс", + "fieldRequired": "Потрібна назва поля", + "dropped": "Індекс \"{name}\" видалено", + "dropFailed": "Не вдалося видалити індекс", + "dropTitle": "Видалити індекс?", + "dropDescription": "Це назавжди видалить індекс \"{name}\". Запити, що використовують цей індекс, сповільняться.", + "dropping": "Видалення...", + "dropConfirm": "Видалити індекс", + "badgeSystem": "системний", + "badgeUnique": "унікальний", + "badgeSparse": "розріджений", + "badgeTtl": "TTL", + "empty": "Індексів не знайдено" + }, + "importDialog": { + "title": "Імпорт документів у {collection}", + "onlyJson": "Підтримуються лише файли .json", + "invalidStructure": "Файл має містити масив JSON або обʼєкт", + "invalidJson": "Некоректний JSON: не вдалося розібрати файл", + "imported": "{count, plural, one {Імпортовано # документ} few {Імпортовано # документи} other {Імпортовано # документів}}", + "importFailed": "Імпорт не вдався", + "dropHint": "Перетягніть або натисніть, щоб завантажити", + "dropSubHint": "Підтримує масив JSON або NDJSON", + "docsCount": "{count, plural, one {# док} few {# док} other {# док}}", + "previewLabel": "Попередній перегляд (перші 3 документи)", + "moreDocs": "... і ще {count} документів", + "cancel": "Скасувати", + "importing": "Імпортування...", + "importCount": "{count, plural, one {Імпортувати # док} few {Імпортувати # док} other {Імпортувати # док}}", + "importBtn": "Імпортувати" + }, + "schemaView": { + "analyzing": "Аналіз схеми...", + "loadFailed": "Не вдалося проаналізувати схему", + "retry": "Повторити", + "noDocs": "Немає документів для аналізу", + "sampled": "Проаналізовано {docs} документів · {fields} полів", + "colField": "Поле", + "colTypes": "Типи", + "colCoverage": "Покриття" } }, "ApiClient": { @@ -1764,7 +1849,8 @@ "curlPasted": "cURL вставлено та розібрано успішно", "responseCopied": "Відповідь скопійовано до буфера обміну", "codeCopied": "Код скопійовано до буфера обміну", - "copyFailed": "Не вдалося скопіювати до буфера обміну" + "copyFailed": "Не вдалося скопіювати до буфера обміну", + "curlCopied": "Команду cURL скопійовано" }, "layout": { "collections": "Колекції", @@ -1778,7 +1864,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Надсилання...", "send": "Надіслати", - "invalidJsonBodyHelp": "Неможливо надіслати: тіло JSON недійсне" + "invalidJsonBodyHelp": "Неможливо надіслати: тіло JSON недійсне", + "copyCurl": "Копіювати як cURL" }, "requestTabs": { "params": "Параметри", @@ -1915,7 +2002,10 @@ "placeholderName": "Мій запит", "labelFolder": "Тека", "placeholderFolder": "Обрати теку", - "save": "Зберегти" + "save": "Зберегти", + "newFolder": "Нова папка", + "newFolderPlaceholder": "Назва папки", + "create": "Створити" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3225,21 @@ "executing": "Виконання запиту…", "emptyTitle": "Виконайте запит для перегляду результатів", "emptyHint": "Натисніть ⌘↩ або кнопку Виконати", - "toastNoConnection": "Немає активного підключення." + "toastNoConnection": "Немає активного підключення.", + "btnHistory": "Історія", + "btnSaveQuery": "Зберегти запит", + "savedSection": "Збережені", + "recentSection": "Останні", + "emptyHistory": "Поки що порожньо — виконайте або збережіть запит", + "savePlaceholder": "Назва запиту", + "deleteSaved": "Видалити", + "toastQuerySaved": "Запит збережено" }, "results": { "filterPlaceholder": "Фільтр результатів…", + "editHint": "Двічі клацніть клітинку для редагування", + "toastRowUpdated": "Рядок оновлено", + "toastRowsUpdated": "{count, plural, one {Оновлено # рядок} few {Оновлено # рядки} many {Оновлено # рядків} other {Оновлено # рядка}}", "rowCount": "{count} {count, plural, one {рядок} few {рядки} many {рядків} other {рядків}}", "rowCountFiltered": "{filtered} / {total} рядків", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/vi.json b/apps/desktop-ui/messages/vi.json index 3182e4e8..7cc54820 100644 --- a/apps/desktop-ui/messages/vi.json +++ b/apps/desktop-ui/messages/vi.json @@ -1580,7 +1580,13 @@ "cancel": "Hủy", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {Không thể xóa # bộ sưu tập} other {Không thể xóa # bộ sưu tập}}", + "bulkDeleted": "{count, plural, one {Đã xóa # bộ sưu tập} other {Đã xóa # bộ sưu tập}}", + "bulkDeleteButton": "{count, plural, one {Xóa # bộ sưu tập} other {Xóa # bộ sưu tập}}", + "bulkDeleteFailed": "{count, plural, other {Không xóa được # bộ sưu tập}}", + "bulkDeleted": "{count, plural, other {Đã xóa # bộ sưu tập}}", + "bulkDeleteButton": "Xóa {count, plural, other {# bộ sưu tập}}" }, "document": { "docsBreadcrumb": "{n} tài liệu", @@ -1657,7 +1663,43 @@ "explainReturned": "{n} được trả về", "explainDocsExamined": "{n} được kiểm tra", "explainCollscanHint": "Truy vấn này quét mọi tài liệu trong collection. Cân nhắc tạo chỉ mục trên các trường được lọc.", - "explainRawLabel": "Kết quả explain" + "explainRawLabel": "Kết quả explain", + "docInsertFailed": "Không thể chèn tài liệu", + "docUpdateFailed": "Không thể cập nhật tài liệu", + "indexesLoadFail": "Không thể tải chỉ mục", + "bulkDeleted": "{count, plural, one {Đã xóa # tài liệu} other {Đã xóa # tài liệu}}", + "bulkDeleteFailed": "Xóa hàng loạt thất bại", + "selectedCount": "{count, plural, one {Đã chọn # tài liệu} other {Đã chọn # tài liệu}}", + "deleteSelected": "Xóa mục đã chọn", + "clearSelection": "Xóa", + "statusLoading": "Đang tải…", + "statusShowing": "Hiển thị {from}–{to} trong {total} tài liệu", + "statusEmpty": "0 tài liệu", + "statusSelected": "Đã chọn {count}", + "bulkDeleteTitle": "Xóa {count, plural, one {# tài liệu} other {# tài liệu}}?", + "bulkDeleteDescription": "Thao tác này sẽ xóa vĩnh viễn {count, plural, one {# tài liệu} other {# tài liệu}} khỏi {collection}. Không thể hoàn tác.", + "bulkDeleting": "Đang xóa…", + "bulkDeleteConfirm": "Xóa tất cả", + "queryErrorTitle": "Không thể tải tài liệu", + "retry": "Thử lại", + "docInsertFailed": "Không chèn được tài liệu", + "docUpdateFailed": "Không cập nhật được tài liệu", + "indexesLoadFail": "Không tải được chỉ mục", + "bulkDeleted": "{count, plural, other {Đã xóa # tài liệu}}", + "bulkDeleteFailed": "Xóa hàng loạt thất bại", + "selectedCount": "{count, plural, other {Đã chọn # tài liệu}}", + "deleteSelected": "Xóa mục đã chọn", + "clearSelection": "Xóa lựa chọn", + "statusLoading": "Đang tải...", + "statusShowing": "Đang hiển thị {from}-{to} trong số {total} tài liệu", + "statusEmpty": "0 tài liệu", + "statusSelected": "Đã chọn {count}", + "bulkDeleteTitle": "Xóa {count, plural, other {# tài liệu}}?", + "bulkDeleteDescription": "Thao tác này sẽ xóa vĩnh viễn {count, plural, other {# tài liệu}} khỏi {collection}. Không thể hoàn tác.", + "bulkDeleting": "Đang xóa...", + "bulkDeleteConfirm": "Xóa tất cả", + "queryErrorTitle": "Không tải được tài liệu", + "retry": "Thử lại" }, "tabs": { "filterActive": "Bộ lọc đang áp dụng", @@ -1733,7 +1775,9 @@ "cancel": "Hủy", "export": "Xuất", "sheetName": "Dữ liệu", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "Chỉ xuất trang hiện tại ({count, plural, one {# tài liệu} other {# tài liệu}})", + "pageOnlyNote": "Chỉ xuất trang hiện tại ({count, plural, other {# tài liệu}})" }, "jsonTree": { "typeLabel": "Kiểu: {type}", @@ -1748,6 +1792,132 @@ "previewResult": "Kết quả xem trước ({count})", "previewEmpty": "Không có tài liệu ở giai đoạn này", "previewFail": "Xem trước thất bại" + }, + "indexManager": { + "loading": "Đang tải chỉ mục…", + "retry": "Thử lại", + "countLabel": "{count, plural, one {# chỉ mục} other {# chỉ mục}}", + "totalSize": "tổng {size}", + "statsDocs": "{count, plural, one {# tài liệu} other {# tài liệu}}", + "statsStorage": "{size} lưu trữ", + "statsAvgObj": "{size} TB/tài liệu", + "newIndex": "Chỉ mục mới", + "createTitle": "Tạo chỉ mục", + "fieldPlaceholder": "Tên trường", + "ascending": "Tăng dần", + "descending": "Giảm dần", + "addField": "Thêm trường", + "unique": "Duy nhất", + "sparse": "Sparse", + "ttlLabel": "TTL (giây)", + "ttlPlaceholder": "vd. 3600", + "cancel": "Hủy", + "create": "Tạo", + "creating": "Đang tạo…", + "created": "Đã tạo chỉ mục", + "createFailed": "Không thể tạo chỉ mục", + "fieldRequired": "Tên trường là bắt buộc", + "dropped": "Đã xóa chỉ mục \"{name}\"", + "dropFailed": "Không thể xóa chỉ mục", + "dropTitle": "Xóa chỉ mục?", + "dropDescription": "Thao tác này sẽ xóa vĩnh viễn chỉ mục \"{name}\". Các truy vấn dùng chỉ mục này sẽ chậm hơn.", + "dropping": "Đang xóa…", + "dropConfirm": "Xóa chỉ mục", + "badgeSystem": "hệ thống", + "badgeUnique": "duy nhất", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Không tìm thấy chỉ mục" + }, + "importDialog": { + "title": "Nhập tài liệu vào {collection}", + "onlyJson": "Chỉ hỗ trợ tệp .json", + "invalidStructure": "Tệp phải chứa mảng hoặc đối tượng JSON", + "invalidJson": "JSON không hợp lệ: không thể phân tích tệp", + "imported": "{count, plural, one {Đã nhập # tài liệu} other {Đã nhập # tài liệu}}", + "importFailed": "Nhập thất bại", + "dropHint": "Kéo và thả hoặc nhấp để tải lên", + "dropSubHint": "Hỗ trợ mảng JSON hoặc NDJSON", + "docsCount": "{count, plural, one {# tài liệu} other {# tài liệu}}", + "previewLabel": "Xem trước (3 tài liệu đầu)", + "moreDocs": "… và {count} tài liệu nữa", + "cancel": "Hủy", + "importing": "Đang nhập…", + "importCount": "{count, plural, one {Nhập # tài liệu} other {Nhập # tài liệu}}", + "importBtn": "Nhập" + }, + "schemaView": { + "analyzing": "Đang phân tích lược đồ…", + "loadFailed": "Không thể phân tích lược đồ", + "retry": "Thử lại", + "noDocs": "Không có tài liệu để phân tích", + "sampled": "Đã lấy mẫu {docs} tài liệu · {fields} trường", + "colField": "Trường", + "colTypes": "Kiểu", + "colCoverage": "Độ phủ" + }, + "indexManager": { + "loading": "Đang tải chỉ mục...", + "retry": "Thử lại", + "countLabel": "{count, plural, other {# chỉ mục}}", + "totalSize": "{size} tổng cộng", + "statsDocs": "{count, plural, other {# tài liệu}}", + "statsStorage": "{size} lưu trữ", + "statsAvgObj": "{size} TB/tài liệu", + "newIndex": "Chỉ mục mới", + "createTitle": "Tạo chỉ mục", + "fieldPlaceholder": "Tên trường", + "ascending": "Tăng dần", + "descending": "Giảm dần", + "addField": "Thêm trường", + "unique": "Duy nhất", + "sparse": "Sparse", + "ttlLabel": "TTL (giây)", + "ttlPlaceholder": "ví dụ: 3600", + "cancel": "Hủy", + "create": "Tạo", + "creating": "Đang tạo...", + "created": "Đã tạo chỉ mục", + "createFailed": "Không tạo được chỉ mục", + "fieldRequired": "Tên trường là bắt buộc", + "dropped": "Đã xóa chỉ mục \"{name}\"", + "dropFailed": "Không xóa được chỉ mục", + "dropTitle": "Xóa chỉ mục?", + "dropDescription": "Thao tác này sẽ xóa vĩnh viễn chỉ mục \"{name}\". Các truy vấn dùng chỉ mục này sẽ chậm hơn.", + "dropping": "Đang xóa...", + "dropConfirm": "Xóa chỉ mục", + "badgeSystem": "hệ thống", + "badgeUnique": "duy nhất", + "badgeSparse": "sparse", + "badgeTtl": "TTL", + "empty": "Không tìm thấy chỉ mục nào" + }, + "importDialog": { + "title": "Nhập tài liệu vào {collection}", + "onlyJson": "Chỉ hỗ trợ tệp .json", + "invalidStructure": "Tệp phải chứa một mảng hoặc đối tượng JSON", + "invalidJson": "JSON không hợp lệ: không thể phân tích tệp", + "imported": "{count, plural, other {Đã nhập # tài liệu}}", + "importFailed": "Nhập thất bại", + "dropHint": "Kéo và thả hoặc nhấp để tải lên", + "dropSubHint": "Hỗ trợ mảng JSON hoặc NDJSON", + "docsCount": "{count, plural, other {# tài liệu}}", + "previewLabel": "Xem trước (3 tài liệu đầu tiên)", + "moreDocs": "... và {count} tài liệu khác", + "cancel": "Hủy", + "importing": "Đang nhập...", + "importCount": "{count, plural, other {Nhập # tài liệu}}", + "importBtn": "Nhập" + }, + "schemaView": { + "analyzing": "Đang phân tích lược đồ...", + "loadFailed": "Không phân tích được lược đồ", + "retry": "Thử lại", + "noDocs": "Không có tài liệu để phân tích", + "sampled": "Đã lấy mẫu {docs} tài liệu · {fields} trường", + "colField": "Trường", + "colTypes": "Loại", + "colCoverage": "Độ phủ" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "Đã dán và phân tích cURL thành công", "responseCopied": "Đã sao chép phản hồi vào bộ nhớ tạm", "codeCopied": "Đã sao chép mã vào bộ nhớ tạm", - "copyFailed": "Sao chép vào clipboard không thành công" + "copyFailed": "Sao chép vào clipboard không thành công", + "curlCopied": "Đã sao chép lệnh cURL" }, "layout": { "collections": "Bộ sưu tập", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Đang gửi...", "send": "Gửi", - "invalidJsonBodyHelp": "Không thể gửi: nội dung JSON không hợp lệ" + "invalidJsonBodyHelp": "Không thể gửi: nội dung JSON không hợp lệ", + "copyCurl": "Sao chép dưới dạng cURL" }, "requestTabs": { "params": "Tham số", @@ -1915,7 +2087,10 @@ "placeholderName": "Yêu cầu của tôi", "labelFolder": "Thư mục", "placeholderFolder": "Chọn thư mục", - "save": "Lưu" + "save": "Lưu", + "newFolder": "Thư mục mới", + "newFolderPlaceholder": "Tên thư mục", + "create": "Tạo" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "Đang thực thi truy vấn…", "emptyTitle": "Chạy truy vấn để xem kết quả", "emptyHint": "Nhấn ⌘↩ hoặc nhấp Chạy", - "toastNoConnection": "Không có kết nối hoạt động." + "toastNoConnection": "Không có kết nối hoạt động.", + "btnHistory": "Lịch sử", + "btnSaveQuery": "Lưu truy vấn", + "savedSection": "Đã lưu", + "recentSection": "Gần đây", + "emptyHistory": "Chưa có gì — hãy chạy hoặc lưu một truy vấn", + "savePlaceholder": "Tên truy vấn", + "deleteSaved": "Xóa", + "toastQuerySaved": "Đã lưu truy vấn" }, "results": { "filterPlaceholder": "Lọc kết quả…", + "editHint": "Nhấp đúp vào ô để chỉnh sửa", + "toastRowUpdated": "Đã cập nhật hàng", + "toastRowsUpdated": "{count, plural, other {Đã cập nhật # hàng}}", "rowCount": "{count} hàng", "rowCountFiltered": "{filtered} / {total} hàng", "exportCsv": "CSV", diff --git a/apps/desktop-ui/messages/zh.json b/apps/desktop-ui/messages/zh.json index f1be69ca..42b1770e 100644 --- a/apps/desktop-ui/messages/zh.json +++ b/apps/desktop-ui/messages/zh.json @@ -1580,7 +1580,13 @@ "cancel": "取消", "confirmDeleteConnectionDesc": "This will permanently remove this connection. The database itself will not be affected.", "confirmDropDbDesc": "This will permanently delete the database and all its collections. This cannot be undone.", - "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone." + "confirmDropCollectionDesc": "This will permanently delete the collection and all its documents. This cannot be undone.", + "bulkDeleteFailed": "{count, plural, one {删除 # 个集合失败} other {删除 # 个集合失败}}", + "bulkDeleted": "{count, plural, one {已删除 # 个集合} other {已删除 # 个集合}}", + "bulkDeleteButton": "{count, plural, one {删除 # 个集合} other {删除 # 个集合}}", + "bulkDeleteFailed": "{count, plural, other {删除 # 个集合失败}}", + "bulkDeleted": "{count, plural, other {已删除 # 个集合}}", + "bulkDeleteButton": "删除{count, plural, other { # 个集合}}" }, "document": { "docsBreadcrumb": "{n} 条文档", @@ -1657,7 +1663,43 @@ "explainReturned": "返回 {n} 条", "explainDocsExamined": "检查 {n} 条", "explainCollscanHint": "此查询会扫描集合中的每个文档。考虑在筛选字段上创建索引。", - "explainRawLabel": "explain 输出" + "explainRawLabel": "explain 输出", + "docInsertFailed": "插入文档失败", + "docUpdateFailed": "更新文档失败", + "indexesLoadFail": "加载索引失败", + "bulkDeleted": "{count, plural, one {已删除 # 个文档} other {已删除 # 个文档}}", + "bulkDeleteFailed": "批量删除失败", + "selectedCount": "{count, plural, one {已选 # 个文档} other {已选 # 个文档}}", + "deleteSelected": "删除所选", + "clearSelection": "清除", + "statusLoading": "加载中…", + "statusShowing": "显示第 {from}–{to} 条,共 {total} 个文档", + "statusEmpty": "0 个文档", + "statusSelected": "已选 {count} 个", + "bulkDeleteTitle": "删除 {count, plural, one {# 个文档} other {# 个文档}}?", + "bulkDeleteDescription": "这将从 {collection} 中永久删除 {count, plural, one {# 个文档} other {# 个文档}}。此操作无法撤销。", + "bulkDeleting": "删除中…", + "bulkDeleteConfirm": "全部删除", + "queryErrorTitle": "加载文档失败", + "retry": "重试", + "docInsertFailed": "插入文档失败", + "docUpdateFailed": "更新文档失败", + "indexesLoadFail": "加载索引失败", + "bulkDeleted": "{count, plural, other {已删除 # 个文档}}", + "bulkDeleteFailed": "批量删除失败", + "selectedCount": "{count, plural, other {已选择 # 个文档}}", + "deleteSelected": "删除所选", + "clearSelection": "清除", + "statusLoading": "加载中…", + "statusShowing": "正在显示第 {from}–{to} 个,共 {total} 个文档", + "statusEmpty": "0 个文档", + "statusSelected": "已选择 {count} 个", + "bulkDeleteTitle": "删除{count, plural, other { # 个文档}}?", + "bulkDeleteDescription": "这将从 {collection} 中永久删除{count, plural, other { # 个文档}}。此操作无法撤消。", + "bulkDeleting": "正在删除…", + "bulkDeleteConfirm": "全部删除", + "queryErrorTitle": "加载文档失败", + "retry": "重试" }, "tabs": { "filterActive": "已应用筛选", @@ -1733,7 +1775,9 @@ "cancel": "取消", "export": "导出", "sheetName": "数据", - "formatJson": "JSON" + "formatJson": "JSON", + "pageOnlyNote": "仅导出当前页({count, plural, one {# 个文档} other {# 个文档}})", + "pageOnlyNote": "仅导出当前页({count, plural, other {# 个文档}})" }, "jsonTree": { "typeLabel": "类型:{type}", @@ -1748,6 +1792,132 @@ "previewResult": "预览结果({count})", "previewEmpty": "此阶段没有文档", "previewFail": "预览失败" + }, + "indexManager": { + "loading": "加载索引中…", + "retry": "重试", + "countLabel": "{count, plural, one {# 个索引} other {# 个索引}}", + "totalSize": "共 {size}", + "statsDocs": "{count, plural, one {# 个文档} other {# 个文档}}", + "statsStorage": "{size} 存储", + "statsAvgObj": "{size} 平均/文档", + "newIndex": "新建索引", + "createTitle": "创建索引", + "fieldPlaceholder": "字段名", + "ascending": "升序", + "descending": "降序", + "addField": "添加字段", + "unique": "唯一", + "sparse": "稀疏", + "ttlLabel": "TTL(秒)", + "ttlPlaceholder": "如 3600", + "cancel": "取消", + "create": "创建", + "creating": "创建中…", + "created": "索引已创建", + "createFailed": "创建索引失败", + "fieldRequired": "字段名为必填项", + "dropped": "索引“{name}”已删除", + "dropFailed": "删除索引失败", + "dropTitle": "删除索引?", + "dropDescription": "这将永久删除索引“{name}”。使用该索引的查询将变慢。", + "dropping": "删除中…", + "dropConfirm": "删除索引", + "badgeSystem": "系统", + "badgeUnique": "唯一", + "badgeSparse": "稀疏", + "badgeTtl": "TTL", + "empty": "未找到索引" + }, + "importDialog": { + "title": "将文档导入到 {collection}", + "onlyJson": "仅支持 .json 文件", + "invalidStructure": "文件必须包含 JSON 数组或对象", + "invalidJson": "无效的 JSON:无法解析文件", + "imported": "{count, plural, one {已导入 # 个文档} other {已导入 # 个文档}}", + "importFailed": "导入失败", + "dropHint": "拖放或点击上传", + "dropSubHint": "支持 JSON 数组或 NDJSON", + "docsCount": "{count, plural, one {# 个文档} other {# 个文档}}", + "previewLabel": "预览(前 3 个文档)", + "moreDocs": "… 以及另外 {count} 个文档", + "cancel": "取消", + "importing": "导入中…", + "importCount": "{count, plural, one {导入 # 个文档} other {导入 # 个文档}}", + "importBtn": "导入" + }, + "schemaView": { + "analyzing": "分析结构中…", + "loadFailed": "分析结构失败", + "retry": "重试", + "noDocs": "没有可分析的文档", + "sampled": "已采样 {docs} 个文档 · {fields} 个字段", + "colField": "字段", + "colTypes": "类型", + "colCoverage": "覆盖率" + }, + "indexManager": { + "loading": "正在加载索引…", + "retry": "重试", + "countLabel": "{count, plural, other {# 个索引}}", + "totalSize": "共 {size}", + "statsDocs": "{count, plural, other {# 个文档}}", + "statsStorage": "{size} 存储", + "statsAvgObj": "{size} 平均/文档", + "newIndex": "新建索引", + "createTitle": "创建索引", + "fieldPlaceholder": "字段名称", + "ascending": "升序", + "descending": "降序", + "addField": "添加字段", + "unique": "唯一", + "sparse": "稀疏", + "ttlLabel": "TTL(秒)", + "ttlPlaceholder": "例如 3600", + "cancel": "取消", + "create": "创建", + "creating": "正在创建…", + "created": "索引已创建", + "createFailed": "创建索引失败", + "fieldRequired": "字段名称为必填项", + "dropped": "已删除索引 \"{name}\"", + "dropFailed": "删除索引失败", + "dropTitle": "删除索引?", + "dropDescription": "这将永久删除索引 \"{name}\"。使用此索引的查询将变慢。", + "dropping": "正在删除…", + "dropConfirm": "删除索引", + "badgeSystem": "系统", + "badgeUnique": "唯一", + "badgeSparse": "稀疏", + "badgeTtl": "TTL", + "empty": "未找到索引" + }, + "importDialog": { + "title": "将文档导入到 {collection}", + "onlyJson": "仅支持 .json 文件", + "invalidStructure": "文件必须包含 JSON 数组或对象", + "invalidJson": "无效的 JSON:无法解析文件", + "imported": "{count, plural, other {已导入 # 个文档}}", + "importFailed": "导入失败", + "dropHint": "拖放或点击上传", + "dropSubHint": "支持 JSON 数组或 NDJSON", + "docsCount": "{count, plural, other {# 个文档}}", + "previewLabel": "预览(前 3 个文档)", + "moreDocs": "……以及另外 {count} 个文档", + "cancel": "取消", + "importing": "正在导入…", + "importCount": "{count, plural, other {导入 # 个文档}}", + "importBtn": "导入" + }, + "schemaView": { + "analyzing": "正在分析结构…", + "loadFailed": "分析结构失败", + "retry": "重试", + "noDocs": "没有可分析的文档", + "sampled": "已抽样 {docs} 个文档 · {fields} 个字段", + "colField": "字段", + "colTypes": "类型", + "colCoverage": "覆盖率" } }, "ApiClient": { @@ -1764,7 +1934,8 @@ "curlPasted": "cURL 已粘贴并解析成功", "responseCopied": "响应已复制到剪贴板", "codeCopied": "代码已复制到剪贴板", - "copyFailed": "复制到剪贴板失败" + "copyFailed": "复制到剪贴板失败", + "curlCopied": "已复制 cURL 命令" }, "layout": { "collections": "集合", @@ -1778,7 +1949,8 @@ "urlPlaceholder": "https://api.example.com/v1/...", "sending": "发送中…", "send": "发送", - "invalidJsonBodyHelp": "无法发送:JSON 正文无效" + "invalidJsonBodyHelp": "无法发送:JSON 正文无效", + "copyCurl": "复制为 cURL" }, "requestTabs": { "params": "参数", @@ -1915,7 +2087,10 @@ "placeholderName": "我的请求", "labelFolder": "文件夹", "placeholderFolder": "选择文件夹", - "save": "保存" + "save": "保存", + "newFolder": "新建文件夹", + "newFolderPlaceholder": "文件夹名称", + "create": "创建" }, "shortcuts": { "ariaLabel": "Keyboard shortcuts", @@ -3135,10 +3310,21 @@ "executing": "正在执行查询…", "emptyTitle": "运行查询以查看结果", "emptyHint": "按 ⌘↩ 或点击运行", - "toastNoConnection": "无活动连接。" + "toastNoConnection": "无活动连接。", + "btnHistory": "历史", + "btnSaveQuery": "保存查询", + "savedSection": "已保存", + "recentSection": "最近", + "emptyHistory": "这里还没有内容——运行或保存一个查询", + "savePlaceholder": "查询名称", + "deleteSaved": "删除", + "toastQuerySaved": "查询已保存" }, "results": { "filterPlaceholder": "筛选结果…", + "editHint": "双击单元格进行编辑", + "toastRowUpdated": "行已更新", + "toastRowsUpdated": "{count, plural, other {已更新 # 行}}", "rowCount": "{count} 行", "rowCountFiltered": "{filtered} / {total} 行", "exportCsv": "CSV", diff --git a/apps/desktop-ui/src/app/app/database-explorer/page.tsx b/apps/desktop-ui/src/app/app/database-explorer/page.tsx index 95c98c47..920abf0c 100644 --- a/apps/desktop-ui/src/app/app/database-explorer/page.tsx +++ b/apps/desktop-ui/src/app/app/database-explorer/page.tsx @@ -177,23 +177,6 @@ export default function NoSQLExplorerPage() { } }, [activeTabId, isInitialized, user]); - // Auto-connect logic is now handled by the sidebar tree view mostly, - // but we might want to keep the "initial load" behavior if needed. - // For now, let's rely on the sidebar to list connections. - - const handleConnect = async (connectionString: string) => { - // This is now used by the "Add Connection" dialog - try { - // We just verify connection here, saving is done by ConnectionForm if we update it - // Actually ConnectionForm handles saving. We just need to close dialog and refresh sidebar. - setIsConnectionDialogOpen(false); - setHasConnections(true); - // Sidebar will refresh itself or we trigger a refresh - } catch (error: any) { - toast.error(error.message); - } - }; - const handleSelectCollection = async (connection: SavedConnection, dbName: string, collectionName: string) => { const tabId = `${connection.id}-${dbName}-${collectionName}`; const existingTab = tabs.find((t) => t.id === tabId); @@ -321,51 +304,44 @@ export default function NoSQLExplorerPage() { const handleInsert = async (doc: any) => { if (!activeTab) return; - try { - const conn = await getConnectionForTab(activeTab); + const conn = await getConnectionForTab(activeTab); - const res = await apiFetch("/api/nosql/documents", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - connectionString: conn.connectionString, - readOnly: conn.readOnly ?? false, - dbName: activeTab.dbName, - collectionName: activeTab.collectionName, - document: doc, - }), - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - handleRefresh(); - } catch (error: any) { - throw new Error(error.message); - } + const res = await apiFetch("/api/nosql/documents", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + connectionString: conn.connectionString, + readOnly: conn.readOnly ?? false, + dbName: activeTab.dbName, + collectionName: activeTab.collectionName, + document: doc, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + handleRefresh(); }; - const handleUpdate = async (id: string, update: any) => { + const handleUpdate = async (id: string, update: any, mode: "merge" | "replace" = "merge") => { if (!activeTab) return; - try { - const conn = await getConnectionForTab(activeTab); + const conn = await getConnectionForTab(activeTab); - const res = await apiFetch("/api/nosql/documents", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - connectionString: conn.connectionString, - readOnly: conn.readOnly ?? false, - dbName: activeTab.dbName, - collectionName: activeTab.collectionName, - documentId: id, - update, - }), - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - handleRefresh(); - } catch (error: any) { - throw new Error(error.message); - } + const res = await apiFetch("/api/nosql/documents", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + connectionString: conn.connectionString, + readOnly: conn.readOnly ?? false, + dbName: activeTab.dbName, + collectionName: activeTab.collectionName, + documentId: id, + update, + mode, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + handleRefresh(); }; const handleDelete = async (id: string) => { @@ -815,6 +791,7 @@ export default function NoSQLExplorerPage() { sortField={activeTab.sortField} sortDirection={activeTab.sortDirection} loading={activeTab.loading} + error={activeTab.error} onRefresh={handleRefresh} onInsert={handleInsert} onUpdate={handleUpdate} diff --git a/apps/desktop-ui/src/app/app/sql-client/page.tsx b/apps/desktop-ui/src/app/app/sql-client/page.tsx index 7c18022a..7d7bafa0 100644 --- a/apps/desktop-ui/src/app/app/sql-client/page.tsx +++ b/apps/desktop-ui/src/app/app/sql-client/page.tsx @@ -106,7 +106,7 @@ export default function SqlClientPage() { openQueryTab(conn); }; - const openQueryTab = (conn: SavedSqlConnection, initialQuery?: string) => { + const openQueryTab = (conn: SavedSqlConnection, initialQuery?: string, table?: QueryTab["table"]) => { setActiveConnectionId(conn.id); const id = newTabId(); const newTab: QueryTab = { @@ -118,6 +118,7 @@ export default function SqlClientPage() { result: null, error: null, loading: false, + table, }; setTabs((prev) => [...prev, newTab]); setActiveTabId(id); @@ -126,7 +127,7 @@ export default function SqlClientPage() { const handleSelectTable = (conn: SavedSqlConnection, table: TableInfo) => { setMobileSidebarOpen(false); const query = `SELECT *\nFROM ${table.schema !== "public" && table.schema ? `"${table.schema}".` : ""}"${table.name}"\nLIMIT 100;`; - openQueryTab(conn, query); + openQueryTab(conn, query, { schema: table.schema, name: table.name }); }; const handleSelectConnection = (conn: SavedSqlConnection) => { @@ -264,7 +265,15 @@ export default function SqlClientPage() { key={activeTab.id} tab={activeTab} connection={activeConnection} - onQueryChange={(q) => updateTab(activeTab.id, { query: q })} + onQueryChange={(q) => + // Editing the query text detaches the tab from its source + // table — grid edits must never target a table the rows + // no longer come from. + updateTab(activeTab.id, { + query: q, + table: q === activeTab.query ? activeTab.table : undefined, + }) + } onResult={(result, error) => updateTab(activeTab.id, { result, error })} onClose={() => closeTab(activeTab.id)} /> diff --git a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx index ea0abad6..99be9f73 100644 --- a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx +++ b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx @@ -165,6 +165,8 @@ export const TaskContainer = () => { const handleKeyDown = (event: KeyboardEvent) => { // `event.key` is undefined for IME composition / autofill events — bail early. if (!event.key) return; + // Inactive tool tabs stay mounted under display:none — ignore global shortcuts there. + if (!searchInputRef.current || searchInputRef.current.offsetParent === null) return; const target = event.target as HTMLElement | null; const isTypingInField = target?.tagName === "INPUT" || diff --git a/apps/desktop-ui/src/app/globals.css b/apps/desktop-ui/src/app/globals.css index e581c0d6..0ab5234a 100644 --- a/apps/desktop-ui/src/app/globals.css +++ b/apps/desktop-ui/src/app/globals.css @@ -792,6 +792,9 @@ body { linear-gradient(hsl(var(--border) / 0.3) 1px, transparent 1px), linear-gradient(90deg, hsl(var(--border) / 0.3) 1px, transparent 1px); background-size: 60px 60px; + /* Tool panels sit inset in the padded main column — round the surface so + the grid + ambient glow don't end in sharp corners. */ + border-radius: 0.75rem; } .dark .dashboard-grid-bg { @@ -862,15 +865,18 @@ body { inset: 0 0 auto 0; height: 26rem; pointer-events: none; + /* Match the parent surface's rounding — roots without overflow-hidden + would otherwise paint the glow with sharp top corners. */ + border-radius: inherit; background: - radial-gradient(58% 100% at 12% 0%, rgba(99, 102, 241, 0.16), transparent 68%), - radial-gradient(50% 100% at 50% 0%, rgba(124, 58, 237, 0.10), transparent 70%), - radial-gradient(58% 100% at 90% 0%, rgba(34, 211, 238, 0.12), transparent 70%); + radial-gradient(58% 100% at 12% 0%, hsl(var(--primary) / 0.16), transparent 68%), + radial-gradient(50% 100% at 50% 0%, hsl(var(--primary) / 0.09), transparent 70%), + radial-gradient(58% 100% at 90% 0%, hsl(var(--primary) / 0.12), transparent 70%); } .dark .dash-ambient { background: radial-gradient(60% 100% at 18% 0%, hsl(var(--primary) / 0.16), transparent 70%), - radial-gradient(55% 100% at 88% 0%, rgba(79, 208, 230, 0.12), transparent 70%); + radial-gradient(55% 100% at 88% 0%, hsl(var(--primary) / 0.12), transparent 70%); } /* Tool card gradient sheen revealed on hover (transform/opacity only) */ @@ -880,7 +886,7 @@ body { inset: 0; border-radius: inherit; padding: 1px; - background: linear-gradient(130deg, hsl(var(--primary) / 0.55), transparent 40%, transparent 60%, rgba(79, 208, 230, 0.45)); + background: linear-gradient(130deg, hsl(var(--primary) / 0.55), transparent 40%, transparent 60%, hsl(var(--primary) / 0.4)); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; @@ -900,7 +906,7 @@ body { background: radial-gradient( 240px circle at var(--mx, 50%) var(--my, 50%), hsl(var(--primary) / 0.13), - rgba(79, 208, 230, 0.06) 35%, + hsl(var(--primary) / 0.06) 35%, transparent 60% ); opacity: 0; diff --git a/apps/desktop-ui/src/components/api-client/api-client.tsx b/apps/desktop-ui/src/components/api-client/api-client.tsx index eb4d7b50..3a62396a 100644 --- a/apps/desktop-ui/src/components/api-client/api-client.tsx +++ b/apps/desktop-ui/src/components/api-client/api-client.tsx @@ -26,6 +26,8 @@ import type { SavedExample } from "./types" import { HelpShortcutsDialog } from "./help-shortcuts-dialog" import { SaveRequestDialog } from "./collections/save-request-dialog" import { parseCurlCommand } from "@/utils/curl-parser" +import { generateCode } from "./generate-code" +import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" import { CollectionsSidebar } from "./collections/collections-sidebar" import dynamic from "next/dynamic" import { @@ -117,7 +119,7 @@ function ApiClientInner() { const { format: formatJson } = useJsonFormatter() const { run: runScript } = useScriptsRunner() const { collections } = useCollectionsState() - const { saveRequest } = useCollectionsActions() + const { saveRequest, addFolder, createCollection } = useCollectionsActions() const { history } = useHistoryState() const { addHistoryItem } = useHistoryActions() const { environments, activeEnvId, activeEnvironmentVariables } = useEnvironmentsState() @@ -302,6 +304,14 @@ function ApiClientInner() { } } + const { copyToClipboard } = useCopyToClipboard() + const handleCopyCurl = React.useCallback(() => { + void copyToClipboard(generateCode(activeTab, "curl"), { + successMessage: t("toasts.curlCopied"), + errorMessage: t("toasts.copyFailed"), + }) + }, [activeTab, copyToClipboard, t]) + const handleSaveRequest = (parentId: string, name: string) => { const requestToSave: CollectionRequest = { id: crypto.randomUUID(), @@ -1063,6 +1073,7 @@ function ApiClientInner() { isLoading={activeTab.isLoading} isBodyInvalid={isBodyInvalid} onPaste={handleCurlPaste} + onCopyCurl={handleCopyCurl} urlHistory={urlHistory} tabId={activeTab.id} /> @@ -1220,6 +1231,8 @@ function ApiClientInner() { @@ -1285,6 +1298,8 @@ function ApiClientInner() { diff --git a/apps/desktop-ui/src/components/api-client/collections/save-request-dialog.tsx b/apps/desktop-ui/src/components/api-client/collections/save-request-dialog.tsx index 7604aaed..21d7a9a1 100644 --- a/apps/desktop-ui/src/components/api-client/collections/save-request-dialog.tsx +++ b/apps/desktop-ui/src/components/api-client/collections/save-request-dialog.tsx @@ -21,24 +21,31 @@ import { SelectValue, } from "@/components/ui/select" import { Collection, CollectionFolder, CollectionRequest } from "../types" -import { Save } from "lucide-react" +import { FolderPlus, Save } from "lucide-react" import { useTranslations } from "next-intl" interface SaveRequestDialogProps { collections: Collection[] onSave: (parentId: string, name: string) => void + /** Create a folder inside parentId; resolves with the new folder's id. */ + onCreateFolder?: (parentId: string, name: string) => Promise + /** Create a root collection; resolves with the created collection. */ + onCreateCollection?: (name: string) => Promise defaultName?: string open?: boolean onOpenChange?: (open: boolean) => void } -export function SaveRequestDialog({ collections, onSave, defaultName, open: openProp, onOpenChange }: SaveRequestDialogProps) { +export function SaveRequestDialog({ collections, onSave, onCreateFolder, onCreateCollection, defaultName, open: openProp, onOpenChange }: SaveRequestDialogProps) { const t = useTranslations("ApiClient.saveRequest") const [openInternal, setOpenInternal] = React.useState(false) const open = openProp !== undefined ? openProp : openInternal const setOpen = onOpenChange ?? setOpenInternal const [name, setName] = React.useState(defaultName || "") const [selectedFolderId, setSelectedFolderId] = React.useState("") + const [creatingFolder, setCreatingFolder] = React.useState(false) + const [newFolderName, setNewFolderName] = React.useState("") + const [isCreating, setIsCreating] = React.useState(false) // Flatten folders for selection const getFolders = (items: (CollectionFolder | CollectionRequest)[], prefix = ""): { id: string, name: string }[] => { @@ -77,6 +84,27 @@ export function SaveRequestDialog({ collections, onSave, defaultName, open: open } } + const handleCreateFolder = async () => { + const folderName = newFolderName.trim() + if (!folderName || isCreating) return + setIsCreating(true) + try { + // No folder selected (e.g. nothing exists yet) → create a root + // collection; otherwise nest a folder inside the selection. + if (selectedFolderId && onCreateFolder) { + const id = await onCreateFolder(selectedFolderId, folderName) + if (id) setSelectedFolderId(id) + } else if (onCreateCollection) { + const created = await onCreateCollection(folderName) + if (created) setSelectedFolderId(created.id) + } + setNewFolderName("") + setCreatingFolder(false) + } finally { + setIsCreating(false) + } + } + const isControlled = openProp !== undefined return ( @@ -120,6 +148,45 @@ export function SaveRequestDialog({ collections, onSave, defaultName, open: open ))} + {(onCreateFolder || onCreateCollection) && ( + creatingFolder ? ( +
+ setNewFolderName(e.target.value)} + placeholder={t("newFolderPlaceholder")} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + void handleCreateFolder() + } + if (e.key === "Escape") { + setCreatingFolder(false) + setNewFolderName("") + } + }} + /> + +
+ ) : ( + + ) + )} diff --git a/apps/desktop-ui/src/components/api-client/collections/use-collections.ts b/apps/desktop-ui/src/components/api-client/collections/use-collections.ts index 2b23c908..8c3dc957 100644 --- a/apps/desktop-ui/src/components/api-client/collections/use-collections.ts +++ b/apps/desktop-ui/src/components/api-client/collections/use-collections.ts @@ -179,6 +179,7 @@ export function useCollections() { ]) setCollections((cur) => sortCollections(cur.map((c) => (c.id === updated.id ? updated : c)))) broadcastApiClientUpdate("collections") + return newFolder.id } catch (e) { setCollections(prev) console.error("Error adding folder", e) @@ -490,6 +491,7 @@ export function useCollections() { setCollections((prev) => sortCollections([...prev, created])) broadcastApiClientUpdate("collections") toast.success("Collection created") + return created } catch (e) { console.error("Error creating collection", e) toast.error("Failed to create collection") diff --git a/apps/desktop-ui/src/components/api-client/context/collections-context.tsx b/apps/desktop-ui/src/components/api-client/context/collections-context.tsx index a4ca0c5f..5003f54b 100644 --- a/apps/desktop-ui/src/components/api-client/context/collections-context.tsx +++ b/apps/desktop-ui/src/components/api-client/context/collections-context.tsx @@ -12,11 +12,11 @@ type CollectionsState = { } type CollectionsActions = { - addFolder: (parentId: string, name: string) => Promise + addFolder: (parentId: string, name: string) => Promise deleteItem: (itemId: string) => Promise saveRequest: (parentId: string, request: import("../types").CollectionRequest) => Promise toggleFolder: (folderId: string) => Promise - createCollection: (name: string) => Promise + createCollection: (name: string) => Promise renameCollection: (collectionId: string, name: string) => Promise renameFolder: (folderId: string, name: string) => Promise patchFolder: (folderId: string, patch: Partial) => Promise diff --git a/apps/desktop-ui/src/components/api-client/request-panel.tsx b/apps/desktop-ui/src/components/api-client/request-panel.tsx index d1fed287..90b252d9 100644 --- a/apps/desktop-ui/src/components/api-client/request-panel.tsx +++ b/apps/desktop-ui/src/components/api-client/request-panel.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { Send, Loader2, X, Globe } from "lucide-react" +import { Send, Loader2, X, Globe, Terminal } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -29,6 +29,8 @@ interface RequestPanelProps { /** When true, the Send button is disabled and an SR hint is shown. */ isBodyInvalid?: boolean onPaste: (text: string) => void + /** Copies the current request as a cURL command; button hidden when absent. */ + onCopyCurl?: () => void urlHistory?: string[] /** Pass activeTab.id so local URL state resets on tab switch. */ tabId?: string @@ -68,6 +70,7 @@ function RequestPanelImpl({ isLoading, isBodyInvalid = false, onPaste, + onCopyCurl, urlHistory = [], tabId, }: RequestPanelProps) { @@ -343,6 +346,20 @@ function RequestPanelImpl({ )} + {onCopyCurl && ( + + )} + {isLoading && onCancel ? ( )} @@ -620,6 +664,7 @@ export function DocumentView({ readOnly={readOnly} indexes={indexesData?.indexes || []} totalIndexSize={indexesData?.totalIndexSize} + stats={indexesData?.stats} loading={indexesLoading} error={indexesError} onRefresh={() => { setIndexesData(null); loadIndexes(); }} @@ -658,6 +703,28 @@ export function DocumentView({ + ) : error ? ( +
+
+ +
+
+

{t("queryErrorTitle")}

+

{error}

+
+
+ + {isFilterActive && ( + + )} +
+
) : documents.length === 0 ? (
@@ -736,38 +803,56 @@ export function DocumentView({
) : viewMode === "tree" ? ( - -
- {documents.map((doc, index) => ( -
-
- - {!readOnly && ( - <> - - - - - )} + {!readOnly && ( + <> + + + + + )} +
+ +
- - - ))} + ); + })} -
+ ) : ( /* Table view */
@@ -857,17 +942,11 @@ export function DocumentView({ if (isEditingThis) { return ( - setEditCellValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") commitCellEdit(doc); - if (e.key === "Escape") setEditingCell(null); - }} - onBlur={() => { if (!editCellSaving) setEditingCell(null); }} - className="h-7 text-xs font-mono" + commitCellEdit(doc, raw)} + onCancel={() => setEditingCell(null)} /> ); @@ -964,13 +1043,17 @@ export function DocumentView({ {/* Status bar */}
- {loading ? 'Loading...' : documents.length > 0 - ? `Showing ${(page - 1) * limit + 1}–${Math.min((page - 1) * limit + documents.length, total)} of ${total.toLocaleString()} documents` - : `0 documents` + {loading ? t("statusLoading") : documents.length > 0 + ? t("statusShowing", { + from: (page - 1) * limit + 1, + to: Math.min((page - 1) * limit + documents.length, total), + total: total.toLocaleString(), + }) + : t("statusEmpty") } {selectedIds.size > 0 && ( - {selectedIds.size} selected + {t("statusSelected", { count: selectedIds.size })} )}
@@ -1175,9 +1258,9 @@ export function DocumentView({ - Delete {selectedIds.size} document{selectedIds.size > 1 ? 's' : ''}? + {t("bulkDeleteTitle", { count: selectedIds.size })} - This will permanently delete {selectedIds.size} document{selectedIds.size > 1 ? 's' : ''} from {collectionName}. This cannot be undone. + {t("bulkDeleteDescription", { count: selectedIds.size, collection: collectionName })} @@ -1187,7 +1270,7 @@ export function DocumentView({ disabled={bulkDeleteLoading} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > - {bulkDeleteLoading ? 'Deleting...' : 'Delete All'} + {bulkDeleteLoading ? t("bulkDeleting") : t("bulkDeleteConfirm")} diff --git a/apps/desktop-ui/src/components/nosql-explorer/explorer-sidebar.tsx b/apps/desktop-ui/src/components/nosql-explorer/explorer-sidebar.tsx index 5d1e91c7..ed4b39b3 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/explorer-sidebar.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/explorer-sidebar.tsx @@ -382,9 +382,9 @@ export function ExplorerSidebar({ clearSelection(); if (errors.length > 0) { - toast.error(`Failed to delete ${errors.length} collection(s)`); + toast.error(t("bulkDeleteFailed", { count: errors.length })); } else { - toast.success(`Deleted ${toDelete.length} collection(s)`); + toast.success(t("bulkDeleted", { count: toDelete.length })); } }; @@ -816,7 +816,7 @@ export function ExplorerSidebar({ onClick={() => setBulkDeleteDialog({ open: true })} > - Delete ({selectedCollections.size}) Collection{selectedCollections.size !== 1 ? "s" : ""} + {t("bulkDeleteButton", { count: selectedCollections.size })}
)} diff --git a/apps/desktop-ui/src/components/nosql-explorer/export-dialog.tsx b/apps/desktop-ui/src/components/nosql-explorer/export-dialog.tsx index 259f326e..900d73dd 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/export-dialog.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/export-dialog.tsx @@ -106,6 +106,7 @@ export function ExportDialog({ open, onOpenChange, documents, fields }: ExportDi {t("title")} +

{t("pageOnlyNote", { count: documents.length })}

diff --git a/apps/desktop-ui/src/components/nosql-explorer/import-dialog.tsx b/apps/desktop-ui/src/components/nosql-explorer/import-dialog.tsx index 2a9fa2bc..8d0cd355 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/import-dialog.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/import-dialog.tsx @@ -7,6 +7,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { toast } from "sonner"; import { IconUpload, IconFile, IconX, IconAlertCircle } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; +import { useTranslations } from "next-intl"; interface ImportDialogProps { open: boolean; @@ -16,6 +17,7 @@ interface ImportDialogProps { } export function ImportDialog({ open, onOpenChange, onImport, collectionName }: ImportDialogProps) { + const t = useTranslations("NoSqlExplorer.importDialog"); const [parsed, setParsed] = useState(null); const [parseError, setParseError] = useState(null); const [fileName, setFileName] = useState(null); @@ -31,7 +33,7 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I const processFile = (file: File) => { if (!file.name.endsWith('.json')) { - setParseError('Only .json files are supported'); + setParseError(t("onlyJson")); return; } setFileName(file.name); @@ -47,7 +49,7 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I } else if (typeof data === 'object' && data !== null) { setParsed([data]); } else { - setParseError('File must contain a JSON array or object'); + setParseError(t("invalidStructure")); } } catch { // Try NDJSON (newline-delimited JSON) @@ -56,7 +58,7 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I const docs = lines.map(l => JSON.parse(l)); setParsed(docs); } catch { - setParseError('Invalid JSON: could not parse file'); + setParseError(t("invalidJson")); } } }; @@ -80,11 +82,11 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I setImporting(true); try { await onImport(parsed); - toast.success(`Imported ${parsed.length} documents`); + toast.success(t("imported", { count: parsed.length })); onOpenChange(false); reset(); } catch (err: any) { - toast.error(err.message || 'Import failed'); + toast.error(err.message || t("importFailed")); } finally { setImporting(false); } @@ -101,7 +103,7 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I - Import Documents into {collectionName} + {t("title", { collection: collectionName })}
@@ -124,25 +126,25 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I onChange={handleFileChange} /> -

Drag & drop or click to upload

-

Supports JSON array or NDJSON

+

{t("dropHint")}

+

{t("dropSubHint")}

) : (
{fileName} - {parsed.length.toLocaleString()} docs + {t("docsCount", { count: parsed.length })}
-

Preview (first 3 documents)

+

{t("previewLabel")}

                                         {JSON.stringify(parsed.slice(0, 3), null, 2)}
-                                        {parsed.length > 3 && `\n\n// ... and ${parsed.length - 3} more documents`}
+                                        {parsed.length > 3 && `\n\n// ${t("moreDocs", { count: parsed.length - 3 })}`}
                                     
@@ -158,13 +160,13 @@ export function ImportDialog({ open, onOpenChange, onImport, collectionName }: I
- +
diff --git a/apps/desktop-ui/src/components/nosql-explorer/index-manager.tsx b/apps/desktop-ui/src/components/nosql-explorer/index-manager.tsx index 41f72e94..d0db09e8 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/index-manager.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/index-manager.tsx @@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; -import { IconPlus, IconTrash, IconRefresh, IconDatabase, IconAlertCircle } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; +import { IconPlus, IconTrash, IconRefresh, IconDatabase, IconAlertCircle, IconX } from "@tabler/icons-react"; import { AlertDialog, AlertDialogAction, @@ -17,7 +18,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { cn } from "@/lib/utils"; interface IndexInfo { name: string; @@ -27,12 +27,21 @@ interface IndexInfo { background?: boolean; v?: number; expireAfterSeconds?: number; + size?: number; [key: string]: any; } +interface CollectionStats { + count?: number | null; + size?: number | null; + storageSize?: number | null; + avgObjSize?: number | null; +} + interface IndexManagerProps { indexes: IndexInfo[]; totalIndexSize?: number; + stats?: CollectionStats | null; loading: boolean; error: string | null; onRefresh: () => void; @@ -41,6 +50,11 @@ interface IndexManagerProps { readOnly?: boolean; } +interface FieldRow { + field: string; + dir: "1" | "-1"; +} + function formatKeySpec(key: Record): string { return Object.entries(key) .map(([field, dir]) => `${field}: ${dir === 1 ? 'asc' : dir === -1 ? 'desc' : dir}`) @@ -50,12 +64,14 @@ function formatKeySpec(key: Record): string { function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } export function IndexManager({ indexes, totalIndexSize, + stats, loading, error, onRefresh, @@ -63,22 +79,35 @@ export function IndexManager({ onCreateIndex, readOnly = false, }: IndexManagerProps) { + const t = useTranslations("NoSqlExplorer.indexManager"); const [dropConfirm, setDropConfirm] = useState(null); const [dropping, setDropping] = useState(false); const [creating, setCreating] = useState(false); const [showCreate, setShowCreate] = useState(false); - const [newField, setNewField] = useState(''); - const [newDir, setNewDir] = useState<'1' | '-1'>('1'); + const [fieldRows, setFieldRows] = useState([{ field: '', dir: '1' }]); const [newUnique, setNewUnique] = useState(false); + const [newSparse, setNewSparse] = useState(false); + const [ttlSeconds, setTtlSeconds] = useState(''); + + const resetCreateForm = () => { + setShowCreate(false); + setFieldRows([{ field: '', dir: '1' }]); + setNewUnique(false); + setNewSparse(false); + setTtlSeconds(''); + }; + + const updateRow = (i: number, patch: Partial) => + setFieldRows(rows => rows.map((r, idx) => idx === i ? { ...r, ...patch } : r)); const handleDrop = async () => { if (!dropConfirm) return; setDropping(true); try { await onDropIndex(dropConfirm); - toast.success(`Index "${dropConfirm}" dropped`); + toast.success(t("dropped", { name: dropConfirm })); } catch (err: any) { - toast.error(err.message || 'Failed to drop index'); + toast.error(err.message || t("dropFailed")); } finally { setDropping(false); setDropConfirm(null); @@ -86,23 +115,25 @@ export function IndexManager({ }; const handleCreate = async () => { - if (!newField.trim()) { - toast.error('Field name is required'); + const rows = fieldRows.filter(r => r.field.trim()); + if (rows.length === 0) { + toast.error(t("fieldRequired")); return; } + const keys: Record = {}; + rows.forEach(r => { keys[r.field.trim()] = parseInt(r.dir); }); + const options: Record = {}; + if (newUnique) options.unique = true; + if (newSparse) options.sparse = true; + const ttl = parseInt(ttlSeconds, 10); + if (!isNaN(ttl) && ttl >= 0) options.expireAfterSeconds = ttl; setCreating(true); try { - await onCreateIndex( - { [newField.trim()]: parseInt(newDir) as 1 | -1 }, - newUnique ? { unique: true } : {} - ); - toast.success('Index created'); - setShowCreate(false); - setNewField(''); - setNewDir('1'); - setNewUnique(false); + await onCreateIndex(keys, options); + toast.success(t("created")); + resetCreateForm(); } catch (err: any) { - toast.error(err.message || 'Failed to create index'); + toast.error(err.message || t("createFailed")); } finally { setCreating(false); } @@ -111,7 +142,7 @@ export function IndexManager({ if (loading) { return (
-
Loading indexes...
+
{t("loading")}
); } @@ -124,7 +155,7 @@ export function IndexManager({

{error}

@@ -134,10 +165,19 @@ export function IndexManager({ return (
-
- {indexes.length} {indexes.length === 1 ? 'index' : 'indexes'} - {totalIndexSize !== undefined && ( - · {formatBytes(totalIndexSize)} total +
+ {t("countLabel", { count: indexes.length })} + {typeof totalIndexSize === 'number' && ( + · {t("totalSize", { size: formatBytes(totalIndexSize) })} + )} + {typeof stats?.count === 'number' && ( + · {t("statsDocs", { count: stats.count })} + )} + {typeof stats?.storageSize === 'number' && ( + · {t("statsStorage", { size: formatBytes(stats.storageSize) })} + )} + {typeof stats?.avgObjSize === 'number' && ( + · {t("statsAvgObj", { size: formatBytes(Math.round(stats.avgObjSize)) })} )}
@@ -147,7 +187,7 @@ export function IndexManager({ {!readOnly && ( )}
@@ -155,37 +195,70 @@ export function IndexManager({ {showCreate && (
-

Create Index

-
- setNewField(e.target.value)} - className="h-8 text-xs flex-1" - onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') setShowCreate(false); }} - /> - updateRow(i, { field: e.target.value })} + className="h-8 text-xs flex-1" + onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') resetCreateForm(); }} + /> + + {fieldRows.length > 1 && ( + + )} +
+ ))} +
+ + +
- +
@@ -202,16 +275,19 @@ export function IndexManager({
{idx.name} {idx.name === '_id_' && ( - system + {t("badgeSystem")} )} {idx.unique && ( - unique + {t("badgeUnique")} )} {idx.sparse && ( - sparse + {t("badgeSparse")} )} {idx.expireAfterSeconds !== undefined && ( - TTL + {t("badgeTtl")} + )} + {typeof idx.size === 'number' && ( + {formatBytes(idx.size)} )}

{formatKeySpec(idx.key)}

@@ -234,7 +310,7 @@ export function IndexManager({
-

No indexes found

+

{t("empty")}

)}
@@ -243,19 +319,19 @@ export function IndexManager({ !open && setDropConfirm(null)}> - Drop index? + {t("dropTitle")} - This will permanently drop index {dropConfirm}. Queries using this index will slow down. + {t("dropDescription", { name: dropConfirm ?? "" })} - Cancel + {t("cancel")} - {dropping ? 'Dropping...' : 'Drop Index'} + {dropping ? t("dropping") : t("dropConfirm")} diff --git a/apps/desktop-ui/src/components/nosql-explorer/json-tree.tsx b/apps/desktop-ui/src/components/nosql-explorer/json-tree.tsx index f8f987f6..eee03816 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/json-tree.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/json-tree.tsx @@ -15,7 +15,7 @@ interface JsonTreeProps { defaultExpanded?: boolean; } -export function JsonTree({ data, label, isLast = true, level = 0, defaultExpanded = false }: JsonTreeProps) { +export const JsonTree = React.memo(function JsonTree({ data, label, isLast = true, level = 0, defaultExpanded = false }: JsonTreeProps) { const t = useTranslations("NoSqlExplorer.jsonTree"); const [isExpanded, setIsExpanded] = useState(defaultExpanded || level < 1); @@ -139,4 +139,4 @@ export function JsonTree({ data, label, isLast = true, level = 0, defaultExpande )}
); -} +}); diff --git a/apps/desktop-ui/src/components/nosql-explorer/query-builder.tsx b/apps/desktop-ui/src/components/nosql-explorer/query-builder.tsx index 0935ff11..689fe246 100644 --- a/apps/desktop-ui/src/components/nosql-explorer/query-builder.tsx +++ b/apps/desktop-ui/src/components/nosql-explorer/query-builder.tsx @@ -334,7 +334,7 @@ export function QueryBuilder({ }; const addRule = () => { - setRules([...rules, { id: Math.random().toString(36).substr(2, 9), field: "", operator: "$eq", value: "", type: "auto" }]); + setRules([...rules, { id: crypto.randomUUID(), field: "", operator: "$eq", value: "", type: "auto" }]); }; const removeRule = (id: string) => { @@ -375,7 +375,7 @@ export function QueryBuilder({ } newRules.push({ - id: Math.random().toString(36).substr(2, 9), + id: crypto.randomUUID(), field: key, operator: op as FilterOperator, value: ruleValue, @@ -390,7 +390,7 @@ export function QueryBuilder({ if (ruleType === "objectid") ruleValue = value.$oid; newRules.push({ - id: Math.random().toString(36).substr(2, 9), + id: crypto.randomUUID(), field: key, operator: "$eq", value: ruleValue, @@ -851,6 +851,27 @@ export function QueryBuilder({ {t("format")} )} + {onExplain && ( + + )} ); @@ -68,7 +70,7 @@ export function SchemaView({ if (!data || data.fields.length === 0) { return (
-

No documents to analyze

+

{t("noDocs")}

); } @@ -76,7 +78,7 @@ export function SchemaView({ return (
- Sampled {data.sampleSize} documents · {data.fields.length} fields + {t("sampled", { docs: data.sampleSize, fields: data.fields.length })} @@ -85,9 +87,9 @@ export function SchemaView({ - - - + + + diff --git a/apps/desktop-ui/src/components/notes/NotesSidebar.tsx b/apps/desktop-ui/src/components/notes/NotesSidebar.tsx index 46f7cf17..fad66922 100644 --- a/apps/desktop-ui/src/components/notes/NotesSidebar.tsx +++ b/apps/desktop-ui/src/components/notes/NotesSidebar.tsx @@ -357,6 +357,8 @@ export default function NotesSidebar() { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + // Inactive tool tabs stay mounted under display:none — ignore global shortcuts there. + if (!searchInputRef.current || searchInputRef.current.offsetParent === null) return; if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); searchInputRef.current?.focus(); diff --git a/apps/desktop-ui/src/components/password-manager/password-list.tsx b/apps/desktop-ui/src/components/password-manager/password-list.tsx index 75b12b77..726457de 100644 --- a/apps/desktop-ui/src/components/password-manager/password-list.tsx +++ b/apps/desktop-ui/src/components/password-manager/password-list.tsx @@ -220,6 +220,8 @@ export function PasswordList() { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + // Inactive tool tabs stay mounted under display:none — ignore global shortcuts there. + if (!searchInputRef.current || searchInputRef.current.offsetParent === null) return const target = event.target as HTMLElement | null const isTyping = target?.tagName === "INPUT" || diff --git a/apps/desktop-ui/src/components/shell/top-nav-strip.tsx b/apps/desktop-ui/src/components/shell/top-nav-strip.tsx index 490bdbf3..11449bf9 100644 --- a/apps/desktop-ui/src/components/shell/top-nav-strip.tsx +++ b/apps/desktop-ui/src/components/shell/top-nav-strip.tsx @@ -22,7 +22,6 @@ import { useActiveWorkspace } from '@/store/workspace-store' import { useTabStore } from '@/store/tab-store' import { getRouteConfig } from '@/lib/route-config' import { buildPinnedNavItems, getSidebarToolMeta } from '@/components/sidebar/app-sidebar.helpers' -import { categoryAccent } from '@/components/dashboard/types' import type { NavLink } from '@/components/sidebar/types' import { getToolMessageKey } from '@/lib/tool-i18n' @@ -151,6 +150,9 @@ function PinnedMenu({ type="button" aria-label={`Unpin ${label}`} onPointerDown={(e) => e.stopPropagation()} + // Radix synthesizes a click on the menu item from pointerup when it + // never saw pointerdown — stop pointerup too or the row navigates. + onPointerUp={(e) => e.stopPropagation()} onClick={(e) => { e.preventDefault() e.stopPropagation() @@ -320,7 +322,9 @@ export function TopNavStrip() { // generic route-config lucide icon only when the catalog has none. const meta = getSidebarToolMeta(tab.path) const Icon = meta?.icon ?? config?.icon - const accent = categoryAccent(meta?.category ?? '') + // Tab chip icon follows the user-selected accent (--primary), not the + // fixed per-category color — the ring/glow derive from it via currentColor. + const accent = { bg: 'bg-primary/10', text: 'text-primary' } const isActive = pathname === tab.path const prevActive = i > 0 && tabs[i - 1].path === pathname const showDivider = i > 0 && !isActive && !prevActive diff --git a/apps/desktop-ui/src/components/sql-client/query-editor.tsx b/apps/desktop-ui/src/components/sql-client/query-editor.tsx index 868c3a2d..64e7d875 100644 --- a/apps/desktop-ui/src/components/sql-client/query-editor.tsx +++ b/apps/desktop-ui/src/components/sql-client/query-editor.tsx @@ -1,14 +1,26 @@ "use client"; import { apiFetch } from "@/lib/desktop/api-fetch"; -import { useState, useRef, useCallback } from "react"; +import { useState, useRef, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { IconPlayerPlay, IconLoader2, IconX, IconAlertCircle, + IconHistory, + IconDeviceFloppy, + IconTrash, } from "@tabler/icons-react"; +import { + getSqlQueryHistory, + getSqlSavedQueries, + putSqlQueryHistory, + putSqlSavedQueries, + type NosqlSavedQuery, +} from "@/lib/user-preferences-api"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; import { QueryResult, QueryTab, SavedSqlConnection } from "./types"; @@ -33,6 +45,74 @@ export function QueryEditor({ tab, connection, onQueryChange, onResult, onClose const [isRunning, setIsRunning] = useState(false); const editorRef = useRef(null); + // Query history + saved queries, keyed by connection + database (same + // generic user-preferences list store the nosql explorer uses). + const [history, setHistory] = useState([]); + const [saved, setSaved] = useState([]); + const [saveName, setSaveName] = useState(""); + const [saveOpen, setSaveOpen] = useState(false); + const listKey = connection?.config + ? { connectionName: tab.connectionName, dbName: connection.config.database } + : null; + const listKeyRef = useRef(listKey); + listKeyRef.current = listKey; + + useEffect(() => { + if (!listKey) return; + let cancelled = false; + Promise.all([getSqlQueryHistory(listKey), getSqlSavedQueries(listKey)]) + .then(([h, s]) => { + if (cancelled) return; + setHistory(h.queries ?? []); + setSaved(s.queries ?? []); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tab.connectionName, connection?.config?.database]); + + const recordHistory = useCallback((query: string) => { + const key = listKeyRef.current; + if (!key) return; + setHistory((prev) => { + const next = [query, ...prev.filter((q) => q !== query)].slice(0, 10); + putSqlQueryHistory({ ...key, queries: next }).catch(() => {}); + return next; + }); + }, []); + + const applyQuery = (q: string) => { + onQueryChange(q); + editorRef.current?.setValue(q); + }; + + const saveCurrentQuery = () => { + const key = listKeyRef.current; + const query = editorRef.current?.getValue() ?? tab.query; + const name = saveName.trim(); + if (!key || !name || !query.trim()) return; + setSaved((prev) => { + const next = [{ name, query }, ...prev.filter((s) => s.name !== name)].slice(0, 50); + putSqlSavedQueries({ ...key, queries: next }).catch(() => {}); + return next; + }); + setSaveName(""); + setSaveOpen(false); + toast.success(t("toastQuerySaved")); + }; + + const deleteSavedQuery = (name: string) => { + const key = listKeyRef.current; + if (!key) return; + setSaved((prev) => { + const next = prev.filter((s) => s.name !== name); + putSqlSavedQueries({ ...key, queries: next }).catch(() => {}); + return next; + }); + }; + const runQuery = useCallback(async () => { if (!connection?.config) { toast.error(t("toastNoConnection")); @@ -53,6 +133,7 @@ export function QueryEditor({ tab, connection, onQueryChange, onResult, onClose const data = await res.json(); if (!res.ok) throw new Error(data.error); onResult(data as QueryResult, null); + recordHistory(query); } catch (err) { const msg = err instanceof Error ? err.message : String(err); onResult(null, msg); @@ -60,7 +141,7 @@ export function QueryEditor({ tab, connection, onQueryChange, onResult, onClose } finally { setIsRunning(false); } - }, [connection, tab.query, onResult, t]); + }, [connection, tab.query, onResult, recordHistory, t]); const handleEditorMount = (ed: editor.IStandaloneCodeEditor) => { editorRef.current = ed; @@ -84,6 +165,78 @@ export function QueryEditor({ tab, connection, onQueryChange, onResult, onClose {dbTypeLabel(tab.connectionType)}
+ + + + + +
+ {saved.length === 0 && history.length === 0 && ( +

{t("emptyHistory")}

+ )} + {saved.length > 0 && ( +
+

{t("savedSection")}

+ {saved.map((s) => ( +
+ + +
+ ))} +
+ )} + {history.length > 0 && ( +
+

{t("recentSection")}

+ {history.map((q, i) => ( + + ))} +
+ )} +
+
+
+ + + + + + setSaveName(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") saveCurrentQuery(); }} + /> + + +
- {filteredRows.length !== result.rows.length - ? t("rowCountFiltered", { filtered: filteredRows.length, total: result.rows.length }) + {canEdit && {t("editHint")}} + {filteredIdx.length !== result.rows.length + ? t("rowCountFiltered", { filtered: filteredIdx.length, total: result.rows.length }) : t("rowCount", { count: result.rowCount })}{" "} · {result.executionTime}ms @@ -101,12 +211,15 @@ export function ResultsTable({ result }: ResultsTableProps) { className="px-3 py-1.5 text-left font-medium border-b border-r whitespace-nowrap font-mono" > {col} + {canEdit && pkCols.includes(col) && ( + PK + )} ))}
- {filteredRows.length === 0 ? ( + {filteredIdx.length === 0 ? ( ) : ( - filteredRows.map((row, i) => ( - + filteredIdx.map((origIdx, displayIdx) => ( + {result.columns.map((col) => { - const val = row[col]; + const val = cellValue(origIdx, col); const isNull = val === null || val === undefined; + const isEditing = editing?.row === origIdx && editing?.col === col; + if (isEditing) { + return ( + + ); + } return (
FieldTypesCoverage{t("colField")}{t("colTypes")}{t("colCoverage")}
- {i + 1} + {displayIdx + 1} + setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void commitEdit(origIdx, col); + if (e.key === "Escape") setEditing(null); + }} + onBlur={() => { if (!saving) setEditing(null); }} + /> + { + // PK cells are the row identity — not editable in place. + if (!canEdit || pkCols.includes(col)) return; + setEditing({ row: origIdx, col }); + setEditValue(isNull ? "" : formatCell(val)); + }} > {isNull ? ( NULL diff --git a/apps/desktop-ui/src/components/sql-client/types.ts b/apps/desktop-ui/src/components/sql-client/types.ts index 7e2094b9..6ad3b6f5 100644 --- a/apps/desktop-ui/src/components/sql-client/types.ts +++ b/apps/desktop-ui/src/components/sql-client/types.ts @@ -46,9 +46,16 @@ export interface ColumnInfo { ordinal_position: number; } +export interface PrimaryKeyInfo { + schema: string; + table_name: string; + column_name: string; +} + export interface SchemaInfo { tables: TableInfo[]; columns: ColumnInfo[]; + primaryKeys?: PrimaryKeyInfo[]; } export interface QueryResult { @@ -67,4 +74,6 @@ export interface QueryTab { result: QueryResult | null; error: string | null; loading: boolean; + /** Source table when opened via the schema sidebar — enables grid editing. */ + table?: { schema: string; name: string }; } diff --git a/apps/desktop-ui/src/components/tools/tool-page-header.tsx b/apps/desktop-ui/src/components/tools/tool-page-header.tsx index 1e1cfca0..fdf5495c 100644 --- a/apps/desktop-ui/src/components/tools/tool-page-header.tsx +++ b/apps/desktop-ui/src/components/tools/tool-page-header.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { cn } from '@/lib/utils' -import { CATEGORY_ACCENT } from '@/components/dashboard/types' interface ToolPageHeaderProps { icon: React.ElementType @@ -23,7 +22,10 @@ export function ToolPageHeader({ icon: Icon, title, description, - accent = CATEGORY_ACCENT.Formatters, + // `accent` kept for API compatibility but no longer used — the header icon + // follows the user-selected accent (--primary) on every tool, not the fixed + // per-category color. Section headers still use CATEGORY_ACCENT. + accent: _accent, className, }: ToolPageHeaderProps) { return ( @@ -31,8 +33,7 @@ export function ToolPageHeader({ diff --git a/apps/desktop-ui/src/lib/__tests__/sql-cell.test.ts b/apps/desktop-ui/src/lib/__tests__/sql-cell.test.ts new file mode 100644 index 00000000..7c391b9e --- /dev/null +++ b/apps/desktop-ui/src/lib/__tests__/sql-cell.test.ts @@ -0,0 +1,19 @@ +import { typeCellInput } from "@/lib/sql-cell" + +describe("typeCellInput", () => { + it("auto-types null, booleans and numbers like the nosql grid", () => { + expect(typeCellInput("null")).toBeNull() + expect(typeCellInput("NULL")).toBeNull() + expect(typeCellInput("true")).toBe(true) + expect(typeCellInput("false")).toBe(false) + expect(typeCellInput("42")).toBe(42) + expect(typeCellInput("-3.5")).toBe(-3.5) + }) + + it("keeps everything else as strings", () => { + expect(typeCellInput("hello")).toBe("hello") + expect(typeCellInput("42abc")).toBe("42abc") + expect(typeCellInput("")).toBe("") + expect(typeCellInput("00:30")).toBe("00:30") + }) +}) diff --git a/apps/desktop-ui/src/lib/desktop/api-fetch.ts b/apps/desktop-ui/src/lib/desktop/api-fetch.ts index ab49a7d4..936b9606 100644 --- a/apps/desktop-ui/src/lib/desktop/api-fetch.ts +++ b/apps/desktop-ui/src/lib/desktop/api-fetch.ts @@ -25,11 +25,20 @@ export async function apiFetch(path: string, init?: RequestInit): Promise = { $bucket: '{\n "groupBy": "$field",\n "boundaries": [0, 100],\n "default": "other"\n}', }; -const newId = () => Math.random().toString(36).slice(2, 11); +const newId = () => crypto.randomUUID(); export function newStage(type = "$match"): PipelineStage { return { id: newId(), type, body: STAGE_TEMPLATES[type] ?? "{}", enabled: true }; diff --git a/apps/desktop-ui/src/lib/sql-cell.ts b/apps/desktop-ui/src/lib/sql-cell.ts new file mode 100644 index 00000000..104f50c7 --- /dev/null +++ b/apps/desktop-ui/src/lib/sql-cell.ts @@ -0,0 +1,9 @@ +/** Mirror the nosql-explorer cell commit: auto-type null / booleans / numbers. */ +export function typeCellInput(raw: string): unknown { + const trimmed = raw.trim(); + if (trimmed === "null" || trimmed === "NULL") return null; + if (trimmed === "true") return true; + if (trimmed === "false") return false; + if (trimmed !== "" && /^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed); + return raw; +} diff --git a/apps/desktop-ui/src/lib/user-preferences-api.ts b/apps/desktop-ui/src/lib/user-preferences-api.ts index 68d60fb3..83e234d8 100644 --- a/apps/desktop-ui/src/lib/user-preferences-api.ts +++ b/apps/desktop-ui/src/lib/user-preferences-api.ts @@ -106,3 +106,37 @@ export async function putNosqlSavedQueries(params: { }): Promise { return apiRequest("PUT", `${BASE}/nosql-saved-queries`, params) } + +// ── SQL client — same generic list store, keyed by connection + database ──── + +export async function getSqlQueryHistory(params: { + connectionName: string + dbName: string +}): Promise { + const q = new URLSearchParams({ ...params, collectionName: "" }) + return apiRequest("GET", `${BASE}/sql-query-history?${q.toString()}`) +} + +export async function putSqlQueryHistory(params: { + connectionName: string + dbName: string + queries: string[] +}): Promise { + return apiRequest("PUT", `${BASE}/sql-query-history`, { ...params, collectionName: "" }) +} + +export async function getSqlSavedQueries(params: { + connectionName: string + dbName: string +}): Promise { + const q = new URLSearchParams({ ...params, collectionName: "" }) + return apiRequest("GET", `${BASE}/sql-saved-queries?${q.toString()}`) +} + +export async function putSqlSavedQueries(params: { + connectionName: string + dbName: string + queries: NosqlSavedQuery[] +}): Promise { + return apiRequest("PUT", `${BASE}/sql-saved-queries`, { ...params, collectionName: "" }) +} diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index b98b61b6..483cd870 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -2892,10 +2892,13 @@ name = "mydevtools-desktop" version = "0.1.7" dependencies = [ "base64 0.22.1", + "bytes", "chrono", "cookie_store 0.21.1", "dashmap", "futures-util", + "h2", + "http", "keyring", "mongodb", "mysql_async", @@ -2917,6 +2920,7 @@ dependencies = [ "tauri-plugin-window-state", "thiserror 2.0.18", "tokio", + "tokio-native-tls", "tokio-postgres", "uuid", ] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index efefd878..567eeb55 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -33,7 +33,11 @@ tauri-plugin-process = "2" tokio = { version = "1", features = ["time", "sync", "net", "io-util"] } tokio-postgres = "0.7" postgres-native-tls = "0.5" -native-tls = "0.2" +native-tls = { version = "0.2", features = ["alpn"] } +tokio-native-tls = "0.3" +h2 = "0.4" +http = "1" +bytes = "1" mysql_async = { version = "0.36", default-features = false, features = ["default-rustls-ring"] } mongodb = "3" redis = { version = "0.29", features = ["tokio-comp", "tokio-native-tls-comp"] } diff --git a/apps/desktop/src-tauri/src/dbtools/mongo.rs b/apps/desktop/src-tauri/src/dbtools/mongo.rs index 140b5bb2..77c29da8 100644 --- a/apps/desktop/src-tauri/src/dbtools/mongo.rs +++ b/apps/desktop/src-tauri/src/dbtools/mongo.rs @@ -268,17 +268,27 @@ async fn dispatch(method: &str, rest: &str, req: &Value, conn_str: &str) -> Hand }; let mut update = json_to_doc(&req["update"])?; update.remove("_id"); - let res = client - .database(&db_name) - .collection::(&coll) - .update_one(id_filter(&doc_id), doc! { "$set": update }) - .await - .map_err(|e| e.to_string())?; + let collection = client.database(&db_name).collection::(&coll); + // mode "replace" = full-document editor save (fields removed in the + // editor must be removed in the DB); default "$set" merge = cell edit. + let (matched, modified, upserted) = if req["mode"].as_str() == Some("replace") { + let res = collection + .replace_one(id_filter(&doc_id), update) + .await + .map_err(|e| e.to_string())?; + (res.matched_count, res.modified_count, res.upserted_id) + } else { + let res = collection + .update_one(id_filter(&doc_id), doc! { "$set": update }) + .await + .map_err(|e| e.to_string())?; + (res.matched_count, res.modified_count, res.upserted_id) + }; Ok(ok(&json!({ "result": { "acknowledged": true, - "matchedCount": res.matched_count, - "modifiedCount": res.modified_count, - "upsertedId": res.upserted_id.as_ref().map(bson_to_json), + "matchedCount": matched, + "modifiedCount": modified, + "upsertedId": upserted.as_ref().map(bson_to_json), }}))) } ("DELETE", "/documents") => { @@ -393,13 +403,36 @@ async fn dispatch(method: &str, rest: &str, req: &Value, conn_str: &str) -> Hand Ok(v) => (v[0].to_string(), v[1].to_string()), Err(r) => return Ok(r), }; - let cursor = client - .database(&db_name) - .collection::(&coll) - .list_indexes() - .await - .map_err(|e| e.to_string())?; + let collection = client.database(&db_name).collection::(&coll); + let cursor = collection.list_indexes().await.map_err(|e| e.to_string())?; let indexes: Vec<_> = cursor.try_collect().await.map_err(|e| e.to_string())?; + + // Best-effort sizes/stats via $collStats (views and very old servers + // don't support it — indexes still list, sizes just stay null). + let mut index_sizes = Document::new(); + let mut total_index_size = Value::Null; + let mut stats = Value::Null; + if let Ok(cursor) = collection + .aggregate(vec![doc! { "$collStats": { "storageStats": {} } }]) + .await + { + if let Ok(docs) = cursor.try_collect::>().await { + if let Some(ss) = docs.first().and_then(|d| d.get_document("storageStats").ok()) { + total_index_size = + ss.get("totalIndexSize").map(bson_to_json).unwrap_or(Value::Null); + if let Ok(sizes) = ss.get_document("indexSizes") { + index_sizes = sizes.clone(); + } + stats = json!({ + "count": ss.get("count").map(bson_to_json), + "size": ss.get("size").map(bson_to_json), + "storageSize": ss.get("storageSize").map(bson_to_json), + "avgObjSize": ss.get("avgObjSize").map(bson_to_json), + }); + } + } + } + let list: Vec = indexes .iter() .map(|i| { @@ -407,6 +440,9 @@ async fn dispatch(method: &str, rest: &str, req: &Value, conn_str: &str) -> Hand if let Some(opts) = &i.options { if let Some(name) = &opts.name { v["name"] = json!(name); + if let Some(size) = index_sizes.get(name) { + v["size"] = bson_to_json(size); + } } if opts.unique == Some(true) { v["unique"] = json!(true); @@ -414,11 +450,14 @@ async fn dispatch(method: &str, rest: &str, req: &Value, conn_str: &str) -> Hand if opts.sparse == Some(true) { v["sparse"] = json!(true); } + if let Some(ttl) = opts.expire_after { + v["expireAfterSeconds"] = json!(ttl.as_secs()); + } } v }) .collect(); - Ok(ok(&json!({ "indexes": list, "totalIndexSize": Value::Null }))) + Ok(ok(&json!({ "indexes": list, "totalIndexSize": total_index_size, "stats": stats }))) } ("POST", "/collection/drop") => { let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { @@ -488,7 +527,7 @@ async fn query_documents(client: &Client, req: &Value) -> HandlerResult { Ok(v) => (v[0].to_string(), v[1].to_string()), Err(r) => return Ok(r), }; - let limit = req["limit"].as_i64().unwrap_or(20).clamp(1, 500); + let limit = req["limit"].as_i64().unwrap_or(20).clamp(1, 2000); let skip = req["skip"].as_i64().unwrap_or(0).max(0) as u64; let sort_field = req["sortField"].as_str().unwrap_or(""); let sort_dir: i32 = if req["sortDirection"].as_str() == Some("desc") { -1 } else { 1 }; @@ -541,7 +580,13 @@ async fn query_documents(client: &Client, req: &Value) -> HandlerResult { } let filter = json_to_doc(&parsed)?; - let total = coll.count_documents(filter.clone()).await.map_err(|e| e.to_string())?; + // Empty filter: estimated count (metadata read) instead of a full scan-count + // on every page change. Filtered queries still count exactly. + let total = if filter.is_empty() { + coll.estimated_document_count().await.map_err(|e| e.to_string())? + } else { + coll.count_documents(filter.clone()).await.map_err(|e| e.to_string())? + }; let mut find = coll.find(filter).skip(skip).limit(limit); if !sort_field.is_empty() { find = find.sort(doc! { sort_field: sort_dir }); diff --git a/apps/desktop/src-tauri/src/dbtools/sql.rs b/apps/desktop/src-tauri/src/dbtools/sql.rs index a9ff71a6..0f97072f 100644 --- a/apps/desktop/src-tauri/src/dbtools/sql.rs +++ b/apps/desktop/src-tauri/src/dbtools/sql.rs @@ -203,6 +203,7 @@ pub async fn handle(method: &str, rest: &str, body: Option<&str>) -> HandlerResu "/connect" => connect(&req).await, "/query" => query(&req).await, "/tables" => tables(&req).await, + "/update-row" => update_row(&req).await, _ => Ok(err(404, "Not found")), } } @@ -326,9 +327,23 @@ async fn tables(req: &Value) -> HandlerResult { usize::MAX, ) .await?; + let (primary_keys, ..) = pg_statement( + &client, + "SELECT kcu.table_schema AS schema, kcu.table_name, kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + AND tc.table_name = kcu.table_name + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY kcu.table_schema, kcu.table_name, kcu.ordinal_position", + usize::MAX, + ) + .await?; parse_numeric_fields(&mut tables, &["column_count"]); parse_numeric_fields(&mut columns, &["ordinal_position"]); - Ok(ok(&json!({ "tables": tables, "columns": columns }))) + Ok(ok(&json!({ "tables": tables, "columns": columns, "primaryKeys": primary_keys }))) } else { let mut conn = mysql_connect(&cfg).await?; let (tables, ..) = mysql_statement( @@ -348,7 +363,164 @@ async fn tables(req: &Value) -> HandlerResult { usize::MAX, ) .await?; + let (primary_keys, ..) = mysql_statement( + &mut conn, + "SELECT table_schema AS `schema`, table_name, column_name + FROM information_schema.key_column_usage + WHERE constraint_name = 'PRIMARY' AND table_schema = DATABASE() + ORDER BY table_name, ordinal_position", + usize::MAX, + ) + .await?; let _ = conn.disconnect().await; - Ok(ok(&json!({ "tables": tables, "columns": columns }))) + Ok(ok(&json!({ "tables": tables, "columns": columns, "primaryKeys": primary_keys }))) + } +} + +// ── Row editing ───────────────────────────────────────────────────────────── +// +// The UI builds `where` from the table's primary key, so updates are +// single-row in practice; rowCount is reported back so the client can warn +// if a non-PK filter matched more. + +fn pg_ident(name: &str) -> String { + format!("\"{}\"", name.replace('"', "\"\"")) +} + +fn mysql_ident(name: &str) -> String { + format!("`{}`", name.replace('`', "``")) +} + +/// Postgres literal. Quoted literals are the "unknown" type — the server +/// coerces them to the column type, so strings work for int/date/bool +/// columns too. standard_conforming_strings (default on) makes quote +/// doubling sufficient. +fn pg_literal(v: &Value) -> String { + match v { + Value::Null => "NULL".into(), + Value::Bool(b) => (if *b { "TRUE" } else { "FALSE" }).into(), + Value::Number(n) => n.to_string(), + Value::String(s) => format!("'{}'", s.replace('\'', "''")), + other => format!("'{}'", other.to_string().replace('\'', "''")), + } +} + +fn mysql_param(v: &Value) -> mysql_async::Value { + use mysql_async::Value as M; + match v { + Value::Null => M::NULL, + Value::Bool(b) => M::Int(*b as i64), + Value::Number(n) => n + .as_i64() + .map(M::Int) + .or_else(|| n.as_u64().map(M::UInt)) + .unwrap_or(M::Double(n.as_f64().unwrap_or(0.0))), + Value::String(s) => M::Bytes(s.clone().into_bytes()), + other => M::Bytes(other.to_string().into_bytes()), + } +} + +fn entries(v: &Value) -> Vec<(String, Value)> { + v.as_object() + .map(|m| m.iter().map(|(k, val)| (k.clone(), val.clone())).collect()) + .unwrap_or_default() +} + +async fn update_row(req: &Value) -> HandlerResult { + let cfg = match config_from(req, true) { + Ok(c) => c, + Err(r) => return Ok(r), + }; + let table = req["table"].as_str().unwrap_or(""); + let set = entries(&req["set"]); + let filter = entries(&req["where"]); + if table.is_empty() || set.is_empty() { + return Ok(err(400, "table and set are required")); + } + if filter.is_empty() { + return Ok(err(400, "where is required — refusing to update every row")); + } + let schema = req["schema"].as_str().unwrap_or(""); + + let row_count: u64 = if cfg.ty == "postgresql" { + let target = if schema.is_empty() { + pg_ident(table) + } else { + format!("{}.{}", pg_ident(schema), pg_ident(table)) + }; + let sets: Vec = set + .iter() + .map(|(k, v)| format!("{} = {}", pg_ident(k), pg_literal(v))) + .collect(); + let wheres: Vec = filter + .iter() + .map(|(k, v)| match v { + Value::Null => format!("{} IS NULL", pg_ident(k)), + _ => format!("{} = {}", pg_ident(k), pg_literal(v)), + }) + .collect(); + let sql = format!( + "UPDATE {target} SET {} WHERE {}", + sets.join(", "), + wheres.join(" AND ") + ); + let client = pg_connect(&cfg).await?; + let (_, _, n) = pg_statement(&client, &sql, 0).await?; + n + } else { + use mysql_async::prelude::Queryable; + let target = if schema.is_empty() { + mysql_ident(table) + } else { + format!("{}.{}", mysql_ident(schema), mysql_ident(table)) + }; + let sets: Vec = set.iter().map(|(k, _)| format!("{} = ?", mysql_ident(k))).collect(); + let mut params: Vec = set.iter().map(|(_, v)| mysql_param(v)).collect(); + let mut wheres: Vec = Vec::new(); + for (k, v) in &filter { + if v.is_null() { + wheres.push(format!("{} IS NULL", mysql_ident(k))); + } else { + wheres.push(format!("{} = ?", mysql_ident(k))); + params.push(mysql_param(v)); + } + } + let sql = format!( + "UPDATE {target} SET {} WHERE {}", + sets.join(", "), + wheres.join(" AND ") + ); + let mut conn = mysql_connect(&cfg).await?; + let result = conn + .exec_iter(sql, mysql_async::Params::Positional(params)) + .await + .map_err(|e| e.to_string())?; + let n = result.affected_rows(); + drop(result); + let _ = conn.disconnect().await; + n + }; + + Ok(ok(&json!({ "success": true, "rowCount": row_count }))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identifiers_escape_quotes() { + assert_eq!(pg_ident(r#"we"ird"#), r#""we""ird""#); + assert_eq!(mysql_ident("we`ird"), "`we``ird`"); + } + + #[test] + fn pg_literals_escape_and_type() { + assert_eq!(pg_literal(&json!("O'Brien")), "'O''Brien'"); + assert_eq!(pg_literal(&json!(42)), "42"); + assert_eq!(pg_literal(&json!(true)), "TRUE"); + assert_eq!(pg_literal(&Value::Null), "NULL"); + // Injection attempt stays inside the literal. + assert_eq!(pg_literal(&json!("'; DROP TABLE users; --")), "'''; DROP TABLE users; --'"); } } diff --git a/apps/desktop/src-tauri/src/http/grpc.rs b/apps/desktop/src-tauri/src/http/grpc.rs new file mode 100644 index 00000000..a0ea9145 --- /dev/null +++ b/apps/desktop/src-tauri/src/http/grpc.rs @@ -0,0 +1,279 @@ +//! Native gRPC (HTTP/2) transport for the api-client. +//! +//! The webview does all protobuf work — requests arrive as base64-encoded, +//! already 5-byte-framed gRPC messages (see `lib/grpc-web.ts` sendNativeGrpc). +//! This is a raw h2 transport whose one job reqwest can't do: surface HTTP/2 +//! trailers, where `grpc-status`/`grpc-message` live. ALPN "h2" TLS for +//! https targets; prior-knowledge (h2c) for http targets. Unary, +//! server-streaming and client-streaming all reduce to "send N framed +//! messages, return all response DATA bytes + trailers". + +use base64::Engine; +use serde_json::{json, Map, Value}; +use std::time::{Duration, Instant}; +use tokio::io::{AsyncRead, AsyncWrite}; + +const DEFAULT_TIMEOUT_MS: u64 = 30_000; +const MAX_TIMEOUT_MS: u64 = 600_000; + +trait AsyncRw: AsyncRead + AsyncWrite + Unpin + Send {} +impl AsyncRw for T {} + +fn b64() -> base64::engine::general_purpose::GeneralPurpose { + base64::engine::general_purpose::STANDARD +} + +fn error_envelope(msg: &str) -> Value { + json!({ "status": 502, "body": "", "headers": {}, "trailers": {}, "error": msg }) +} + +/// Entry point: mirrors the web `/api/proxy-grpc` JSON envelope. Errors are +/// returned in-envelope (`{error}`), never thrown — the client treats them as +/// soft failures. +pub async fn proxy_grpc(input: Value) -> Value { + let started = Instant::now(); + let timeout_ms = input["timeoutMs"] + .as_u64() + .unwrap_or(DEFAULT_TIMEOUT_MS) + .clamp(1, MAX_TIMEOUT_MS); + match tokio::time::timeout(Duration::from_millis(timeout_ms), run(&input)).await { + Ok(Ok(mut envelope)) => { + envelope["timeMs"] = json!(started.elapsed().as_millis() as u64); + envelope + } + Ok(Err(e)) => error_envelope(&e), + Err(_) => error_envelope(&format!("gRPC request timed out after {timeout_ms}ms")), + } +} + +async fn run(input: &Value) -> Result { + let url = input["url"].as_str().ok_or("missing url")?; + let parsed = reqwest::Url::parse(url).map_err(|e| format!("invalid url: {e}"))?; + crate::http::proxy::assert_hop_allowed(&parsed)?; + let https = parsed.scheme() == "https"; + let host = parsed.host_str().ok_or("url has no host")?.to_string(); + let port = parsed.port().unwrap_or(if https { 443 } else { 80 }); + + // Request frames — already 5-byte framed by the webview. + let mut frames: Vec> = Vec::new(); + if let Some(list) = input["bodyFrames"].as_array() { + for f in list { + frames.push( + b64().decode(f.as_str().unwrap_or("")) + .map_err(|e| format!("bad frame base64: {e}"))?, + ); + } + } else if let Some(body) = input["body"].as_str() { + frames.push(b64().decode(body).map_err(|e| format!("bad body base64: {e}"))?); + } + if frames.is_empty() { + frames.push(Vec::new()); + } + + let tcp = tokio::net::TcpStream::connect((host.as_str(), port)) + .await + .map_err(|e| format!("connect to {host}:{port} failed: {e}"))?; + let io: Box = if https { + let connector = native_tls::TlsConnector::builder() + .request_alpns(&["h2"]) + .build() + .map_err(|e| format!("TLS setup failed: {e}"))?; + let connector = tokio_native_tls::TlsConnector::from(connector); + Box::new( + connector + .connect(&host, tcp) + .await + .map_err(|e| format!("TLS handshake failed: {e}"))?, + ) + } else { + Box::new(tcp) + }; + + let (client, connection) = h2::client::handshake(io) + .await + .map_err(|e| format!("HTTP/2 handshake failed (server may not speak h2): {e}"))?; + tokio::spawn(async move { + let _ = connection.await; + }); + let mut client = client.ready().await.map_err(|e| e.to_string())?; + + let mut builder = http::Request::builder() + .method("POST") + .uri(url) + .header("content-type", "application/grpc") + .header("te", "trailers") + .header("user-agent", "mydevtools-desktop-grpc"); + if let Some(headers) = input["headers"].as_object() { + for (k, v) in headers { + let key = k.to_lowercase(); + // Connection-specific headers are forbidden in HTTP/2. + if ["host", "connection", "content-length", "transfer-encoding", "te", "keep-alive", "upgrade"] + .contains(&key.as_str()) + { + continue; + } + if let Some(value) = v.as_str() { + builder = builder.header(k.as_str(), value); + } + } + } + let request = builder.body(()).map_err(|e| format!("bad request: {e}"))?; + + let (response_fut, mut send_stream) = + client.send_request(request, false).map_err(|e| e.to_string())?; + let last = frames.len() - 1; + for (i, frame) in frames.into_iter().enumerate() { + send_frame(&mut send_stream, frame, i == last).await?; + } + + let response = response_fut + .await + .map_err(|e| format!("gRPC request failed: {e}"))?; + let status = response.status().as_u16(); + let mut headers_map = Map::new(); + for (k, v) in response.headers() { + headers_map.insert(k.to_string(), json!(v.to_str().unwrap_or(""))); + } + + let mut recv = response.into_body(); + let mut body_bytes: Vec = Vec::new(); + while let Some(chunk) = std::future::poll_fn(|cx| recv.poll_data(cx)).await { + let chunk = chunk.map_err(|e| format!("stream error: {e}"))?; + let _ = recv.flow_control().release_capacity(chunk.len()); + body_bytes.extend_from_slice(&chunk); + } + let mut trailers_map = Map::new(); + if let Ok(Some(trailers)) = std::future::poll_fn(|cx| recv.poll_trailers(cx)).await { + for (k, v) in trailers.iter() { + trailers_map.insert(k.to_string(), json!(v.to_str().unwrap_or(""))); + } + } + + // grpc-status normally arrives in trailers; trailers-only responses put it + // straight on the headers. + let pick = |key: &str| -> Option { + trailers_map + .get(key) + .or_else(|| headers_map.get(key)) + .and_then(Value::as_str) + .map(String::from) + }; + let grpc_status = pick("grpc-status").and_then(|s| s.parse::().ok()); + let grpc_message = pick("grpc-message"); + + let mut envelope = json!({ + "status": status, + "body": b64().encode(&body_bytes), + "headers": headers_map, + "trailers": trailers_map, + "sizeBytes": body_bytes.len(), + }); + if let Some(s) = grpc_status { + envelope["grpcStatus"] = json!(s); + } + if let Some(m) = grpc_message { + envelope["grpcMessage"] = json!(m); + } + Ok(envelope) +} + +/// Send one framed message respecting h2 flow control (chunked to the window). +async fn send_frame( + stream: &mut h2::SendStream, + data: Vec, + end_stream: bool, +) -> Result<(), String> { + let mut buf = bytes::Bytes::from(data); + if buf.is_empty() { + return stream.send_data(buf, end_stream).map_err(|e| e.to_string()); + } + while !buf.is_empty() { + stream.reserve_capacity(buf.len()); + let available = std::future::poll_fn(|cx| stream.poll_capacity(cx)) + .await + .ok_or("stream closed while sending request body")? + .map_err(|e| e.to_string())?; + if available == 0 { + continue; + } + let chunk = buf.split_to(available.min(buf.len())); + let is_last = end_stream && buf.is_empty(); + stream + .send_data(chunk, is_last) + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use http::{HeaderMap, HeaderValue, Response, StatusCode}; + use tokio::net::TcpListener; + + #[tokio::test] + async fn rejects_missing_and_invalid_urls() { + let e = proxy_grpc(json!({})).await; + assert_eq!(e["error"], "missing url"); + let e = proxy_grpc(json!({ "url": "ftp://x/Svc/M" })).await; + assert!(e["error"].as_str().unwrap().contains("Blocked scheme")); + } + + #[tokio::test] + async fn blocks_metadata_hosts() { + let e = proxy_grpc(json!({ "url": "http://169.254.169.254/Svc/M" })).await; + assert!(e["error"].as_str().unwrap().contains("Blocked target")); + } + + /// End-to-end over h2c: loopback h2 server echoes one DATA frame and + /// grpc-status trailers; client envelope must carry both. + #[tokio::test] + async fn h2c_roundtrip_with_trailers() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + let (socket, _) = listener.accept().await.unwrap(); + let mut conn = h2::server::handshake(socket).await.unwrap(); + if let Some(result) = conn.accept().await { + let (request, mut respond) = result.unwrap(); + // The Connection must keep being polled to drive stream I/O. + tokio::spawn(async move { while conn.accept().await.is_some() {} }); + assert_eq!(request.uri().path(), "/pkg.Svc/Method"); + let mut body = request.into_body(); + // Drain the request body. + while let Some(chunk) = std::future::poll_fn(|cx| body.poll_data(cx)).await { + let c = chunk.unwrap(); + let _ = body.flow_control().release_capacity(c.len()); + } + let response = Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/grpc") + .body(()) + .unwrap(); + let mut send = respond.send_response(response, false).unwrap(); + send.send_data(bytes::Bytes::from_static(b"\x00\x00\x00\x00\x02hi"), false) + .unwrap(); + let mut trailers = HeaderMap::new(); + trailers.insert("grpc-status", HeaderValue::from_static("0")); + trailers.insert("grpc-message", HeaderValue::from_static("OK")); + send.send_trailers(trailers).unwrap(); + } + }); + + let input = json!({ + "url": format!("http://127.0.0.1:{port}/pkg.Svc/Method"), + "headers": { "authorization": "Bearer t" }, + "body": b64().encode(b"\x00\x00\x00\x00\x01x"), + "timeoutMs": 3000, + }); + let envelope = proxy_grpc(input).await; + assert!(envelope["error"].is_null(), "unexpected error: {envelope}"); + assert_eq!(envelope["status"], 200); + assert_eq!(envelope["grpcStatus"], 0); + assert_eq!(envelope["grpcMessage"], "OK"); + assert_eq!(envelope["trailers"]["grpc-status"], "0"); + let body = b64().decode(envelope["body"].as_str().unwrap()).unwrap(); + assert_eq!(&body, b"\x00\x00\x00\x00\x02hi"); + } +} diff --git a/apps/desktop/src-tauri/src/http/mod.rs b/apps/desktop/src-tauri/src/http/mod.rs index 7dc28812..cd400f6a 100644 --- a/apps/desktop/src-tauri/src/http/mod.rs +++ b/apps/desktop/src-tauri/src/http/mod.rs @@ -1,4 +1,5 @@ pub mod auth_server; +pub mod grpc; pub mod mock_server; pub mod proxy; pub mod remote; diff --git a/apps/desktop/src-tauri/src/http/proxy.rs b/apps/desktop/src-tauri/src/http/proxy.rs index d17a1076..6e6580ee 100644 --- a/apps/desktop/src-tauri/src/http/proxy.rs +++ b/apps/desktop/src-tauri/src/http/proxy.rs @@ -41,7 +41,7 @@ fn envelope_error(status: u16, status_text: &str, msg: &str) -> Value { }) } -fn assert_hop_allowed(url: &reqwest::Url) -> Result<(), String> { +pub(crate) fn assert_hop_allowed(url: &reqwest::Url) -> Result<(), String> { match url.scheme() { "http" | "https" => {} s => return Err(format!("Blocked scheme: {s}:")), diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 06ca7224..4edf1cc8 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -65,6 +65,11 @@ async fn mock_server_start(app: tauri::AppHandle) -> Result { http::mock_server::start(app).await } +#[tauri::command] +async fn proxy_grpc(input: serde_json::Value) -> Result { + Ok(http::grpc::proxy_grpc(input).await) +} + #[tauri::command] async fn await_browser_auth( port_channel: tauri::ipc::Channel, @@ -124,6 +129,7 @@ pub fn run() { http_request_stream, http_request_stream_cancel, mock_server_start, + proxy_grpc, await_browser_auth ]) .run(tauri::generate_context!()) diff --git a/apps/desktop/src-tauri/src/router/preferences.rs b/apps/desktop/src-tauri/src/router/preferences.rs index e43f59ca..1dc6f7ca 100644 --- a/apps/desktop/src-tauri/src/router/preferences.rs +++ b/apps/desktop/src-tauri/src/router/preferences.rs @@ -181,6 +181,13 @@ pub fn handle( (m, "/nosql-saved-queries") => { nosql_query_list(&db, m, "nosql_saved_queries", MAX_NOSQL_SAVED_QUERIES, query, body) } + // SQL client reuses the same generic list store (collectionName = ""). + (m, "/sql-query-history") => { + nosql_query_list(&db, m, "sql_query_history", MAX_NOSQL_HISTORY_QUERIES, query, body) + } + (m, "/sql-saved-queries") => { + nosql_query_list(&db, m, "sql_saved_queries", MAX_NOSQL_SAVED_QUERIES, query, body) + } _ => Ok(ApiResponse::detail(404, "Not found")), } }