diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a914b..561f0dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2026-06-13 + +### Added + +- 🔌 **Tool servers.** Connect external tools via MCP or OpenAPI. Add servers from the new Tool Servers admin tab, verify the connection, and the AI can use them immediately. Supports bearer auth and custom headers. +- 🤖 **Sub-agents.** The AI can now spin up sub-agents to work on tasks in parallel. Each sub-agent gets full tool access and runs as a real chat you can inspect afterwards. Configure concurrency and limits from the new Subagents admin tab. +- ⚡ **Parallel tool execution.** When the AI makes multiple tool calls in one response, they now run concurrently instead of one at a time. +- 🔐 **Signup toggle.** Enable or disable user registration directly from the Users admin tab. +- 🧠 **Context compaction threshold in UI.** The token threshold for automatic context compaction can now be set from the Models admin tab instead of editing `config.toml`. + +### Changed + +- 🏗️ **Admin panel restructure.** The old Settings and Browser panels have been split into focused tabs: **Web** (search providers + browser backends), **Tool Servers**, and **Subagents**. +- 🌍 **Comprehensive i18n.** 200+ new translation keys across all 10 locales. Almost every remaining hardcoded English string in the UI is now translatable. +- 🏷️ **External tool labels.** Tool calls from external servers show the tool name and server in the chat UI. Sub-agent tasks show as `Sub-agent: "task…"`. + +### Fixed + +- 🐛 **Plan mode with external tools.** Plan mode now correctly sees tools from connected external servers, not just built-in ones. + ## [0.3.5] - 2026-06-13 ### Added diff --git a/cptr/frontend/package-lock.json b/cptr/frontend/package-lock.json index e421e4e..1f05b5e 100644 --- a/cptr/frontend/package-lock.json +++ b/cptr/frontend/package-lock.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.3.4", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/cptr/frontend/package.json b/cptr/frontend/package.json index d56009c..43b48b5 100644 --- a/cptr/frontend/package.json +++ b/cptr/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.3.5", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite dev", diff --git a/cptr/frontend/src/lib/apis/admin.ts b/cptr/frontend/src/lib/apis/admin.ts index c6243fb..f3c71b8 100644 --- a/cptr/frontend/src/lib/apis/admin.ts +++ b/cptr/frontend/src/lib/apis/admin.ts @@ -128,3 +128,41 @@ export const updateModelConfig = ( ...jsonBody(update), method: 'PUT' }); + +// ── Tool Servers ──────────────────────────────────────────── + +export interface ToolServer { + id: string; + type: 'openapi' | 'mcp'; + url: string; + path: string; + auth_type: string; + key: string; + name: string; + description: string; + headers: Record | null; + enabled: boolean; +} + +export const listToolServers = async (): Promise => { + const data = await fetchJSON<{ servers: ToolServer[] }>('/api/admin/tools/servers'); + return data.servers; +}; + +export const createToolServer = (server: Omit) => + fetchJSON('/api/admin/tools/servers', jsonBody(server)); + +export const updateToolServer = (id: string, updates: Partial>) => + fetchJSON(`/api/admin/tools/servers/${id}`, { + ...jsonBody(updates), + method: 'PUT' + }); + +export const deleteToolServer = (id: string) => + fetchJSON(`/api/admin/tools/servers/${id}`, { method: 'DELETE' }); + +export const verifyToolServer = (id: string) => + fetchJSON<{ ok: boolean; tools?: { name: string; description: string }[]; message?: string }>( + `/api/admin/tools/servers/${id}/verify`, + { method: 'POST' } + ); diff --git a/cptr/frontend/src/lib/components/Admin/AudioSettings.svelte b/cptr/frontend/src/lib/components/Admin/AudioSettings.svelte index 8ae2435..32b6c71 100644 --- a/cptr/frontend/src/lib/components/Admin/AudioSettings.svelte +++ b/cptr/frontend/src/lib/components/Admin/AudioSettings.svelte @@ -52,7 +52,7 @@ toast.success($t('settings.saved')); refreshAudioState(); } catch { - toast.error('Failed to save audio settings'); + toast.error($t('admin.audio.saveFailed')); } finally { saving = false; } @@ -60,53 +60,53 @@
-

Audio

+

{$t('admin.audio.title')}

{#if loading}
{:else} -

Voice Memos

+

{$t('admin.audio.voiceMemos')}

- Record voice memos from the "+" menu. + {$t('admin.audio.voiceMemosHint')}

- {transcribeEnabled ? 'Recordings are transcribed to markdown via STT.' : 'Recordings are saved as audio only.'} + {transcribeEnabled ? $t('admin.audio.transcribeOnHint') : $t('admin.audio.transcribeOffHint')}

- Recording quality + {$t('admin.audio.recordingQuality')}

- {quality === 'high' ? 'Best quality, larger files.' : quality === 'medium' ? 'Balanced quality and size.' : 'Smallest files, optimized for speech.'} + {quality === 'high' ? $t('admin.audio.qualityHintHigh') : quality === 'medium' ? $t('admin.audio.qualityHintMedium') : $t('admin.audio.qualityHintLow')}

-

Speech-to-Text

+

{$t('admin.audio.stt')}

- +
- +
- +

- Compatible with OpenAI's audio/transcriptions API. + {$t('admin.audio.sttHint')}

diff --git a/cptr/frontend/src/lib/components/Admin/CreateBotModal.svelte b/cptr/frontend/src/lib/components/Admin/CreateBotModal.svelte index 31d8dbf..209c07a 100644 --- a/cptr/frontend/src/lib/components/Admin/CreateBotModal.svelte +++ b/cptr/frontend/src/lib/components/Admin/CreateBotModal.svelte @@ -54,16 +54,16 @@ ); let selectedWsName = $derived( - $workspaceList.find((w) => w.path === workspace)?.name || workspace.split('/').pop() || 'Select workspace' + $workspaceList.find((w) => w.path === workspace)?.name || workspace.split('/').pop() || $t('automationModal.selectWorkspace') ); - const platformHints: Record = { - telegram: 'Create a bot via @BotFather', - discord: 'Create a bot in the Developer Portal', - slack: 'Bot Token | App Token (pipe-separated)', - whatsapp: 'Access Token | Phone Number ID (pipe-separated)', - signal: 'signal-cli URL | Phone Number (pipe-separated)' - }; + const platformHints: Record = $derived({ + telegram: $t('messaging.hint.telegram'), + discord: $t('messaging.hint.discord'), + slack: $t('messaging.hint.slack'), + whatsapp: $t('messaging.hint.whatsapp'), + signal: $t('messaging.hint.signal') + }); async function handleVerify() { if (!token.trim()) return; @@ -106,7 +106,7 @@ onsave(); onclose(); } catch (e: any) { - toast.error(e.message || 'Failed to save'); + toast.error(e.message || $t('messaging.failedToSave')); } finally { saving = false; } @@ -122,14 +122,14 @@ }} >

- {bot ? 'Edit Bot' : 'Add Bot'} + {bot ? $t('messaging.editBot') : $t('messaging.addBot')}

{#if !bot}
- +
- + @@ -191,7 +191,7 @@ {#if verifying} {:else} - Verify + {$t('messaging.verify')} {/if} {/if} @@ -208,8 +208,8 @@ {/if} - -

Comma-separated user IDs. Leave empty to allow all.

+ +

{$t('messaging.allowedSendersHint')}

{:else} - {bot ? 'Save' : 'Add'} → + {bot ? $t('messaging.save') : $t('messaging.add')} {/if}
diff --git a/cptr/frontend/src/lib/components/Admin/Gateway.svelte b/cptr/frontend/src/lib/components/Admin/Gateway.svelte index 868127c..bd2c26e 100644 --- a/cptr/frontend/src/lib/components/Admin/Gateway.svelte +++ b/cptr/frontend/src/lib/components/Admin/Gateway.svelte @@ -167,7 +167,7 @@ class="shrink-0 text-[11px] text-gray-500 hover:text-gray-900 dark:text-gray-500 dark:hover:text-white transition-colors" onclick={copyKey} > - Copy + {$t('admin.gateway.copy')}
@@ -183,7 +183,7 @@
{/if} -

Keys

+

{$t('admin.gateway.keys')}

- Base URL: + {$t('admin.gateway.baseUrl')} {`${typeof window !== 'undefined' ? window.location.origin : ''}/v1`}
- API Key: + {$t('admin.gateway.apiKey')} sk-cptr-...
- Headers: + {$t('admin.gateway.headers')}
{/if} diff --git a/cptr/frontend/src/lib/components/Admin/Models.svelte b/cptr/frontend/src/lib/components/Admin/Models.svelte index 49229c1..234a8d8 100644 --- a/cptr/frontend/src/lib/components/Admin/Models.svelte +++ b/cptr/frontend/src/lib/components/Admin/Models.svelte @@ -4,6 +4,8 @@ import { getModelConfig, updateModelConfig, + getAdminConfig, + updateConfig, type ModelConfigEntry } from '$lib/apis/admin'; import { t } from '$lib/i18n'; @@ -32,6 +34,10 @@ let globalExpanded = $state(false); let showVariables = $state(false); + // Context compaction + let compactTokenThreshold = $state(80000); + let compactDirty = $state(false); + const TEMPLATE_VARIABLES = [ { name: 'WORKSPACE_NAME', desc: 'Workspace folder name' }, { name: 'WORKSPACE_PATH', desc: 'Full workspace path' }, @@ -52,7 +58,7 @@ Workspace: {{WORKSPACE_NAME}} Files: {{FILE_TREE}}`; - let hasDirty = $derived(globalDirty || models.some((m) => m.dirty)); + let hasDirty = $derived(globalDirty || compactDirty || models.some((m) => m.dirty)); function parseRows(config: ModelConfigEntry | undefined): ParamRow[] { const rp = config?.params?.request_params; @@ -98,6 +104,12 @@ Files: } finally { loading = false; } + + // Load context compaction threshold from admin config + try { + const adminCfg = await getAdminConfig(); + compactTokenThreshold = Number(adminCfg['chat.compact_token_threshold']) || 80000; + } catch {} }); async function toggleModel(e: Event, model: ModelEntry) { @@ -145,6 +157,13 @@ Files: await Promise.all(promises); globalDirty = false; models.forEach((m) => (m.dirty = false)); + + // Save context compaction threshold + if (compactDirty) { + await updateConfig({ 'chat.compact_token_threshold': compactTokenThreshold }); + compactDirty = false; + } + toast.success($t('settings.saved')); } catch { toast.error($t('models.failedToSave')); @@ -256,6 +275,24 @@ Files:

{$t('admin.models')}

+ + +

{$t('admin.contextCompaction')}

+ +
+
+ +
+ (compactDirty = true)} + min="10000" max="1000000" step="10000" + class="w-24 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors" /> + {$t('admin.compactTokenThresholdUnit')} +
+

{$t('admin.compactTokenThresholdHint')}

+
+
+ -
- {/if} -
diff --git a/cptr/frontend/src/lib/components/Admin/Subagents.svelte b/cptr/frontend/src/lib/components/Admin/Subagents.svelte new file mode 100644 index 0000000..a6c0dd1 --- /dev/null +++ b/cptr/frontend/src/lib/components/Admin/Subagents.svelte @@ -0,0 +1,113 @@ + + +
+

{$t('admin.subagents')}

+ + {#if loading} +
+ {:else} +
+ +

+ {$t('admin.subagentsHint')} +

+ + {#if enabled} +
+ +
+ + {$t('admin.subagentsMaxConcurrentHint')} +
+
+ +
+ +
+ + {$t('admin.subagentsMaxIterationsHint')} +
+
+ +
+ +
+ + chars +
+
+ +
+ + + + + + {#if verifyResult?.ok && verifyResult.tools} +
+ {verifyResult.tools.length} + {$t('toolServers.toolsFound')}: + {verifyResult.tools.map((t) => t.name).join(', ')} +
+ {/if} + + +
+
+ {#if editServer} + + + {/if} +
+ +
+
+ +{/if} diff --git a/cptr/frontend/src/lib/components/Admin/Users.svelte b/cptr/frontend/src/lib/components/Admin/Users.svelte index e9c7030..91de2d8 100644 --- a/cptr/frontend/src/lib/components/Admin/Users.svelte +++ b/cptr/frontend/src/lib/components/Admin/Users.svelte @@ -4,10 +4,11 @@ import CreateUserModal from './CreateUserModal.svelte'; import EditUserModal from './EditUserModal.svelte'; import { onMount } from 'svelte'; - import { listUsers } from '$lib/apis/admin'; + import { listUsers, getAdminConfig, updateConfig } from '$lib/apis/admin'; import { session } from '$lib/session'; import { t } from '$lib/i18n'; import Spinner from '$lib/components/common/Spinner.svelte'; + import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte'; interface User { user_id: string; @@ -24,6 +25,10 @@ let loading = $state(true); let page = $state(0); + // Auth config + let signupEnabled = $state(false); + let savingConfig = $state(false); + let showCreate = $state(false); let editUser = $state(null); @@ -41,6 +46,26 @@ } } + async function loadAuthConfig() { + try { + const config = await getAdminConfig(); + signupEnabled = config['auth.signup_enabled'] === true; + } catch {} + } + + async function toggleSignup(value: boolean) { + savingConfig = true; + try { + await updateConfig({ 'auth.signup_enabled': value }); + signupEnabled = value; + toast.success($t('settings.saved')); + } catch { + toast.error($t('admin.failedToSave')); + } finally { + savingConfig = false; + } + } + function handleCreated() { showCreate = false; loadUsers(); @@ -52,7 +77,10 @@ if (page >= totalPages) page = Math.max(0, totalPages - 1); } - onMount(loadUsers); + onMount(() => { + loadUsers(); + loadAuthConfig(); + });
@@ -65,6 +93,21 @@
+ +
+ +

+ {signupEnabled ? $t('admin.signUpEnabled') : $t('admin.signUpDisabled')} +

+
+ {#if loading}
diff --git a/cptr/frontend/src/lib/components/Admin/Web.svelte b/cptr/frontend/src/lib/components/Admin/Web.svelte new file mode 100644 index 0000000..40a12a5 --- /dev/null +++ b/cptr/frontend/src/lib/components/Admin/Web.svelte @@ -0,0 +1,314 @@ + + +
+

{$t('admin.web')}

+ + {#if loading} +
+ {:else} + +

Search

+ +
+ +

+ {webEnabled ? $t('admin.webEnabledHint') : $t('admin.webDisabledHint')} +

+ + {#if webEnabled} +
+ {$t('admin.webSearchProvider')} + +
+

+ {#if searchProvider === 'auto'} + {$t('admin.webAutoHint')} + {:else if searchProvider === 'duckduckgo'} + {$t('admin.webDuckDuckGoNote')} + {/if} +

+ + {#if searchProvider === 'exa'} +
+ + +

{$t('admin.webExaHint')}

+
+ {:else if searchProvider === 'tavily'} +
+ + +

{$t('admin.webTavilyHint')}

+
+ {:else if searchProvider === 'brave'} +
+ + +

{$t('admin.webBraveHint')}

+
+ {:else if searchProvider === 'perplexity'} +
+ + +

{$t('admin.webPerplexityHint')}

+
+ {:else if searchProvider === 'chat_completions'} +
+ + +
+
+ + +
+
+ + +
+

{$t('admin.webCcHint')}

+ {/if} + {/if} +
+ + +

Browser

+ +
+ +

+ Give the AI access to a web browser for navigating pages, clicking elements, and taking screenshots. +

+ + {#if browserEnabled} +
+ Provider + +
+

+ {#if browserProvider === 'local'} + Connects to Chrome via DevTools Protocol. Full interactive browsing with clicking, typing, and screenshots. + {:else if browserProvider === 'firecrawl'} + Cloud API that converts web pages to markdown. Fast extraction, no interactive browsing. + {:else} + Cloud API for LLM-driven browser tasks. Describe what you need in natural language. + {/if} +

+ + {#if browserProvider === 'local'} + + +
+ +
+ + +
+ {#if testResult} +

+ {testResult.message} +

+ {/if} +
+ +
+ +
+ + minutes +
+
+ {:else if browserProvider === 'firecrawl'} +
+ + +
+
+ + +

Change for self-hosted Firecrawl instances

+
+ {:else if browserProvider === 'browser_use'} +
+ + +
+
+ + +
+ {/if} + {/if} +
+ + +
+ +
+ {/if} +
diff --git a/cptr/frontend/src/lib/components/GitView.svelte b/cptr/frontend/src/lib/components/GitView.svelte index 2084745..d782100 100644 --- a/cptr/frontend/src/lib/components/GitView.svelte +++ b/cptr/frontend/src/lib/components/GitView.svelte @@ -4,6 +4,7 @@ import { gitStatusStore, type GitStatus, type GitFile } from '$lib/stores/gitStatus.svelte'; import { tooltip } from '$lib/tooltip'; + import { t } from '$lib/i18n'; import Icon from './Icon.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; @@ -309,8 +310,8 @@ class="flex h-6 w-6 items-center justify-center rounded-md text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-white/6 dark:hover:text-gray-300" onclick={() => refreshReview(false)} disabled={refreshing} - aria-label="Refresh changes" - use:tooltip={'Refresh changes'} + aria-label={$t('a11y.refreshChanges')} + use:tooltip={$t('a11y.refreshChanges')} > diff --git a/cptr/frontend/src/lib/components/GroupTabBar.svelte b/cptr/frontend/src/lib/components/GroupTabBar.svelte index cf4e553..583648c 100644 --- a/cptr/frontend/src/lib/components/GroupTabBar.svelte +++ b/cptr/frontend/src/lib/components/GroupTabBar.svelte @@ -165,7 +165,7 @@ ...($voiceMemosEnabled ? [ { - label: 'Voice Memo', + label: $t('bar.voiceMemo'), icon: 'microphone', shortcut: formatChord($keybindings.voiceMemo), onclick: () => { @@ -183,12 +183,12 @@ if (isWideScreen && tab.type === 'file' && tab.filePath && !$splitActive) { items.push({ - label: 'Split Right', + label: $t('bar.splitRight'), icon: 'split-horizontal', onclick: () => openInSplit(tab.filePath!, 'horizontal') }); items.push({ - label: 'Split Down', + label: $t('bar.splitDown'), icon: 'split-vertical', onclick: () => openInSplit(tab.filePath!, 'vertical') }); @@ -210,7 +210,7 @@ const direction = $activeWorkspace?.splitDirection ?? 'horizontal'; return [ { - label: 'Split Right', + label: $t('bar.splitRight'), icon: 'split-horizontal', active: direction === 'horizontal', onclick: () => { @@ -219,7 +219,7 @@ } }, { - label: 'Split Down', + label: $t('bar.splitDown'), icon: 'split-vertical', active: direction === 'vertical', onclick: () => { @@ -309,7 +309,7 @@ {/if} - {tab.type === 'files' ? ($activeWorkspace?.name ?? 'Files') : tab.label} + {tab.type === 'files' ? ($activeWorkspace?.name ?? $t('bar.files')) : tab.label} {#if tab.unsaved}{/if} {#if !tab.permanent} @@ -354,8 +354,8 @@ ? 'bg-gray-200/50 text-gray-900 dark:bg-white/8 dark:text-white' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'}" onclick={() => (showSplitMenu = !showSplitMenu)} - aria-label="Split Editor" - use:tooltip={'Split Editor'} + aria-label={$t('a11y.splitEditor')} + use:tooltip={$t('a11y.splitEditor')} > closeGroup(group.id)} - aria-label="Close pane" - use:tooltip={'Close pane'} + aria-label={$t('a11y.closePane')} + use:tooltip={$t('a11y.closePane')} > diff --git a/cptr/frontend/src/lib/components/Icon.svelte b/cptr/frontend/src/lib/components/Icon.svelte index c9261a6..3137450 100644 --- a/cptr/frontend/src/lib/components/Icon.svelte +++ b/cptr/frontend/src/lib/components/Icon.svelte @@ -360,6 +360,13 @@ + {:else if name === 'globe'} + + + + + + {:else if name === 'microphone'} import { onMount } from 'svelte'; + import { t } from '$lib/i18n'; interface Props { onClick?: () => void; @@ -72,7 +73,7 @@
diff --git a/cptr/frontend/src/lib/components/Settings/Browser.svelte b/cptr/frontend/src/lib/components/Settings/Browser.svelte deleted file mode 100644 index 15a6905..0000000 --- a/cptr/frontend/src/lib/components/Settings/Browser.svelte +++ /dev/null @@ -1,254 +0,0 @@ - - -
-

Browser

- - {#if loading} -
- {:else} - -

Enable

- -
- -

- Give the AI access to a web browser for navigating pages, clicking elements, and taking screenshots. -

-
- - {#if enabled} - -

Provider

- -
- {#each [ - { value: 'local' as const, label: 'Local CDP' }, - { value: 'firecrawl' as const, label: 'Firecrawl' }, - { value: 'browser_use' as const, label: 'Browser-Use' } - ] as opt} - - {/each} -
-

- {#if provider === 'local'} - Connects to Chrome via DevTools Protocol. Full interactive browsing with clicking, typing, and screenshots. - {:else if provider === 'firecrawl'} - Cloud API that converts web pages to markdown. Fast extraction, no interactive browsing. - {:else} - Cloud API for LLM-driven browser tasks. Describe what you need in natural language. - {/if} -

- - - {#if provider === 'local'} -

Connection

- -
- - -
- -
- - -
- {#if testResult} -

- {testResult.message} -

- {/if} -
- -
- -
- - minutes -
-
-
- {/if} - - - {#if provider === 'firecrawl'} -

Firecrawl

- -
-
- - -
-
- - -

Change for self-hosted Firecrawl instances

-
-
- {/if} - - - {#if provider === 'browser_use'} -

Browser-Use

- -
-
- - -
-
- - -
-
- {/if} - {/if} - - -
- -
- {/if} -
diff --git a/cptr/frontend/src/lib/components/Settings/Keyboard.svelte b/cptr/frontend/src/lib/components/Settings/Keyboard.svelte index d376088..6aed5ad 100644 --- a/cptr/frontend/src/lib/components/Settings/Keyboard.svelte +++ b/cptr/frontend/src/lib/components/Settings/Keyboard.svelte @@ -2,7 +2,6 @@ import { keybindings, ACTION_IDS, - ACTION_LABELS, DEFAULT_KEYBINDINGS, formatChord, eventToChord, @@ -26,6 +25,22 @@ toggleSidebar: $t('keyboard.toggleSidebar') }); + /** Translated action labels for display. */ + const ACTION_LABELS: Record = $derived({ + newFile: $t('keyboard.action.newFile'), + newTerminal: $t('keyboard.action.newTerminal'), + newChat: $t('keyboard.action.newChat'), + closeTab: $t('keyboard.action.closeTab'), + nextTab: $t('keyboard.action.nextTab'), + prevTab: $t('keyboard.action.prevTab'), + quickOpen: $t('keyboard.action.quickOpen'), + searchAll: $t('keyboard.action.searchAll'), + openSettings: $t('keyboard.action.openSettings'), + toggleSplit: $t('keyboard.action.toggleSplit'), + toggleSidebar: $t('keyboard.action.toggleSidebar'), + voiceMemo: $t('keyboard.action.voiceMemo') + }); + let recordingAction = $state(null); function startRecording(actionId: ActionId) { @@ -116,7 +131,7 @@ @@ -130,7 +145,7 @@ {#if conflict} !! {/if}
diff --git a/cptr/frontend/src/lib/components/SettingsModal.svelte b/cptr/frontend/src/lib/components/SettingsModal.svelte index 936a54b..03b9b9c 100644 --- a/cptr/frontend/src/lib/components/SettingsModal.svelte +++ b/cptr/frontend/src/lib/components/SettingsModal.svelte @@ -4,7 +4,6 @@ import General from './Settings/General.svelte'; import Account from './Settings/Account.svelte'; import Keyboard from './Settings/Keyboard.svelte'; - import Browser from './Settings/Browser.svelte'; import About from './Settings/About.svelte'; import Users from './Admin/Users.svelte'; import Connections from './Admin/Connections.svelte'; @@ -12,14 +11,15 @@ import Messaging from './Admin/Messaging.svelte'; import Gateway from './Admin/Gateway.svelte'; import AudioSettings from './Admin/AudioSettings.svelte'; - import AdminSettings from './Admin/Settings.svelte'; + import AdminWeb from './Admin/Web.svelte'; + import ToolServers from './Admin/ToolServers.svelte'; + import Subagents from './Admin/Subagents.svelte'; import { session } from '$lib/session'; import { t } from '$lib/i18n'; type Tab = | 'general' | 'keyboard' - | 'browser' | 'account' | 'about' | 'users' @@ -28,7 +28,9 @@ | 'messaging' | 'gateway' | 'audio' - | 'admin_settings'; + | 'web' + | 'toolservers' + | 'subagents'; interface Props { onclose: () => void; @@ -54,9 +56,10 @@ { id: 'models', label: $t('admin.models'), icon: 'cube' }, { id: 'messaging', label: $t('admin.messaging'), icon: 'chat-bubble' }, { id: 'gateway', label: $t('admin.gateway.tab'), icon: 'gateway' }, - { id: 'audio', label: 'Audio', icon: 'microphone' }, - { id: 'browser', label: 'Browser', icon: 'browser' }, - { id: 'admin_settings', label: $t('settings.configuration'), icon: 'shield' } + { id: 'audio', label: $t('admin.audio.title'), icon: 'microphone' }, + { id: 'web', label: $t('admin.web'), icon: 'globe' }, + { id: 'toolservers', label: $t('admin.toolServers'), icon: 'plug' }, + { id: 'subagents', label: $t('admin.subagents'), icon: 'user' } ]); @@ -117,8 +120,6 @@ {:else if activeTab === 'keyboard'} - {:else if activeTab === 'browser'} - {:else if activeTab === 'account'} {:else if activeTab === 'about'} @@ -135,8 +136,12 @@ {:else if activeTab === 'audio'} - {:else if activeTab === 'admin_settings'} - + {:else if activeTab === 'web'} + + {:else if activeTab === 'toolservers'} + + {:else if activeTab === 'subagents'} + {/if}
diff --git a/cptr/frontend/src/lib/components/Sidebar.svelte b/cptr/frontend/src/lib/components/Sidebar.svelte index 74f8a44..f20f902 100644 --- a/cptr/frontend/src/lib/components/Sidebar.svelte +++ b/cptr/frontend/src/lib/components/Sidebar.svelte @@ -343,7 +343,7 @@ }} > - Automations + {$t('automations.title')}
{/if} @@ -388,7 +388,7 @@ e.preventDefault(); toggleWorkspaceExpand(ws.path); }} - aria-label={isExpanded ? 'Collapse' : 'Expand'} + aria-label={isExpanded ? $t('sidebar.collapse') : $t('sidebar.addWorkspace')} > @@ -417,8 +417,8 @@ role="button" tabindex="-1" onclick={() => handleNewChat(ws.path)} - aria-label="New Chat" - use:tooltip={'New Chat'} + aria-label={$t('bar.newChat')} + use:tooltip={$t('bar.newChat')} > diff --git a/cptr/frontend/src/lib/components/VoiceMemoModal.svelte b/cptr/frontend/src/lib/components/VoiceMemoModal.svelte index 5be2c0c..ceb31be 100644 --- a/cptr/frontend/src/lib/components/VoiceMemoModal.svelte +++ b/cptr/frontend/src/lib/components/VoiceMemoModal.svelte @@ -7,6 +7,7 @@ import { writeFile } from '$lib/apis/files'; import { fetchJSON } from '$lib/apis'; import { transcribeEnabled, recordingQuality, QUALITY_BITRATES } from '$lib/stores/audio'; + import { t } from '$lib/i18n'; interface Props { workspace: string; @@ -126,7 +127,7 @@ elapsed = 0; timerInterval = window.setInterval(() => elapsed++, 1000); } catch { - toast.error('Could not access microphone'); + toast.error($t('voiceMemo.microphoneError')); onclose(); } } @@ -164,7 +165,7 @@ await fetchJSON('/api/workspace/files/upload', { method: 'POST', body: form }); await clearFromIDB(id).catch(() => {}); } catch { - toast.error('Audio saved locally. Server upload failed.'); + toast.error($t('voiceMemo.uploadFailed')); } // 3. Transcribe (if enabled) @@ -181,7 +182,7 @@ transcript = ''; const msg = err?.message || ''; if (msg.includes('not configured')) { - toast.error('STT not configured. Set up in Settings → Audio.'); + toast.error($t('voiceMemo.sttNotConfigured')); } } } @@ -214,7 +215,7 @@ phase = 'done'; setTimeout(onclose, 500); } catch { - toast.error('Failed to write transcript. Audio is safe.'); + toast.error($t('voiceMemo.writeFailed')); } } @@ -258,17 +259,17 @@
{$t('common.cancel')} {$t('voiceMemo.done')}
{:else if phase === 'processing'}
- Processing… + {$t('voiceMemo.processing')}
{:else if phase === 'naming'} {#if transcript} @@ -277,12 +278,12 @@

{/if} - + Save →{$t('common.save')} → {:else if phase === 'done'} -

Saved ✓

+

{$t('voiceMemo.saved')}

{/if} diff --git a/cptr/frontend/src/lib/components/chat/AssistantMessage.svelte b/cptr/frontend/src/lib/components/chat/AssistantMessage.svelte index 298a846..08ce235 100644 --- a/cptr/frontend/src/lib/components/chat/AssistantMessage.svelte +++ b/cptr/frontend/src/lib/components/chat/AssistantMessage.svelte @@ -6,6 +6,7 @@ import MarkdownRenderer from '$lib/components/markdown/MarkdownRenderer.svelte'; import OutputEditView from './OutputEditView.svelte'; import { currentWorkspace, openFileTab } from '$lib/stores'; + import { t } from '$lib/i18n'; interface Props { content: string; @@ -139,42 +140,58 @@ /** Human-readable label for a tool call */ function toolLabel(name: string, args: any): string { + const _t = $t; switch (name) { case 'read_file': { - const range = args.start_line ? ` L${args.start_line}–${args.end_line || 'end'}` : ''; - return `Read ${shortPath(args.path)}${range}`; + const p = shortPath(args.path); + if (args.start_line) return _t('chat.tool.readFileRange', { path: p, range: `${args.start_line}–${args.end_line || 'end'}` }); + return _t('chat.tool.readFile', { path: p }); } case 'edit_file': - return `Edit ${shortPath(args.path)}`; + return _t('chat.tool.editFile', { path: shortPath(args.path) }); case 'multi_edit_file': - return `Multi-edit ${shortPath(args.path)}`; + return _t('chat.tool.multiEditFile', { path: shortPath(args.path) }); case 'create_file': - return `Create ${shortPath(args.path)}`; + return _t('chat.tool.createFile', { path: shortPath(args.path) }); case 'write_file': - return `Write ${shortPath(args.path)}`; + return _t('chat.tool.writeFile', { path: shortPath(args.path) }); case 'list_directory': - return `List ${shortPath(args.path)}${args.recursive ? ' (recursive)' : ''}`; + return args.recursive + ? _t('chat.tool.listDirectoryRecursive', { path: shortPath(args.path) }) + : _t('chat.tool.listDirectory', { path: shortPath(args.path) }); case 'search_files': { - const scope = args.include ? ` in ${args.include}` : ''; - return `Search "${args.query || '?'}"${scope}`; + const scope = args.include ? _t('chat.tool.searchFilesScope', { include: args.include }) : ''; + return _t('chat.tool.searchFiles', { query: args.query || '?', scope }); } case 'run_command': - return args.background ? `Background: ${args.command || '?'}` : args.command || '?'; + return args.background ? _t('chat.tool.backgroundCommand', { command: args.command || '?' }) : args.command || '?'; case 'check_task': - return `Check task ${args.task_id || '?'}`; + return _t('chat.tool.checkTask', { id: args.task_id || '?' }); case 'kill_task': - return `Kill task ${args.task_id || '?'}`; + return _t('chat.tool.killTask', { id: args.task_id || '?' }); case 'web_search': - return `Search web: "${args.query || '?'}"`; + return _t('chat.tool.webSearch', { query: args.query || '?' }); case 'read_url': { try { - return `Fetch ${new URL(args.url).hostname}`; + return _t('chat.tool.fetchUrl', { hostname: new URL(args.url).hostname }); } catch { - return `Fetch URL`; + return _t('chat.tool.fetchUrlFallback'); } } - default: + case 'delegate_task': { + const t = args.task || '?'; + return `Sub-agent: "${t.length > 60 ? t.slice(0, 60) + '…' : t}"`; + } + default: { + // External tool: {server_id}_{tool_name} → "tool_name (server_id)" + const idx = name.indexOf('_'); + if (idx > 0) { + const serverId = name.slice(0, idx); + const toolName = name.slice(idx + 1); + return `${toolName} (${serverId})`; + } return name; + } } } @@ -329,16 +346,16 @@
{$t('chat.saveAs')}
{$t('common.cancel')} {$t('common.save')}
@@ -395,7 +412,7 @@ /> {artifact.title || 'Artifact'}{artifact.title || $t('chat.artifact')} {#if preview} @@ -419,7 +436,7 @@ {$t('chat.allow')} {$t('chat.deny')} {:else} @@ -720,7 +737,7 @@
- Input + {$t('chat.toolInput')}
{#if toolName === 'edit_file' && args.target} @@ -752,7 +769,7 @@
- Lines {args.start_line}–{args.end_line || 'end'} + {$t('chat.toolLines', { start: args.start_line, end: args.end_line || 'end' })}
{/if} {:else if toolName === 'run_command'} @@ -786,7 +803,7 @@
- Output + {$t('chat.toolOutput')}
-																				{pairedOutput.output.length.toLocaleString()} total characters
+																				{$t('chat.totalChars', { count: pairedOutput.output.length.toLocaleString() })}
 																			
{/if} @@ -829,14 +846,14 @@ bg-gray-100 dark:bg-white/8 hover:bg-gray-200 dark:hover:bg-white/12 transition-colors duration-100" - onclick={() => onapprove(messageId, item.call_id, true)}>Allow onapprove(messageId, item.call_id, true)}>{$t('chat.allow')} onapprove(messageId, item.call_id, false)}>{$t('chat.deny')} @@ -867,7 +884,7 @@ class="p-0.5 rounded text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-default transition-colors duration-100" disabled={siblingIndex === 0} onclick={() => onnavigate?.(-1)} - aria-label="Previous response" + aria-label={$t('chat.prevResponse')} > onnavigate?.(1)} - aria-label="Next response" + aria-label={$t('chat.nextResponse')} > {#if copied} (showUsageTooltip = !showUsageTooltip)} onmouseenter={() => (showUsageTooltip = true)} onmouseleave={() => (showUsageTooltip = false)} - aria-label="Usage info" + aria-label={$t('chat.usageInfo')} > import type { ChatInfo } from '$lib/apis/chat'; + import { t } from '$lib/i18n'; import ChatItem from '../common/ChatItem.svelte'; import DropdownMenu from '../DropdownMenu.svelte'; import Pagination from '../common/Pagination.svelte'; @@ -48,12 +49,12 @@ const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffM = Math.floor(diffMs / 60000); - if (diffM < 1) return 'Just now'; - if (diffM < 60) return `${diffM}m ago`; + if (diffM < 1) return $t('chat.history.justNow'); + if (diffM < 60) return $t('chat.history.minutesAgo', { count: diffM }); const diffH = Math.floor(diffM / 60); - if (diffH < 24) return `${diffH}h ago`; + if (diffH < 24) return $t('chat.history.hoursAgo', { count: diffH }); const diffD = Math.floor(diffH / 24); - if (diffD < 7) return `${diffD}d ago`; + if (diffD < 7) return $t('chat.history.daysAgo', { count: diffD }); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } @@ -100,7 +101,7 @@ class="flex-1 flex items-center gap-1 hover:text-gray-600 dark:hover:text-gray-400 transition-colors" onclick={() => onsort?.('title')} > - Title + {$t('chat.history.title')} {#if sortBy === 'title'} {#if sortDir === 'asc'} {@render chevronUp()} @@ -115,7 +116,7 @@ class="shrink-0 flex items-center gap-1 hover:text-gray-600 dark:hover:text-gray-400 transition-colors" onclick={() => onsort?.('updated_at')} > - Updated + {$t('chat.history.updated')} {#if sortBy === 'updated_at'} {#if sortDir === 'asc'} {@render chevronUp()} @@ -148,7 +149,7 @@ align="end" items={[ { - label: 'Delete', + label: $t('chat.history.delete'), icon: 'trash', onclick: () => { if (menuChatId) ondelete(menuChatId); diff --git a/cptr/frontend/src/lib/components/chat/ChatInput.svelte b/cptr/frontend/src/lib/components/chat/ChatInput.svelte index 7a60586..d7cb37d 100644 --- a/cptr/frontend/src/lib/components/chat/ChatInput.svelte +++ b/cptr/frontend/src/lib/components/chat/ChatInput.svelte @@ -23,6 +23,7 @@ import Icon from '../Icon.svelte'; import { planMode } from '$lib/stores'; import Spinner from '$lib/components/common/Spinner.svelte'; + import { t } from '$lib/i18n'; interface Props { inputText: string; @@ -586,7 +587,7 @@ class="bg-white text-black border border-white rounded-full group-hover:visible invisible transition outline-none shadow-sm flex items-center justify-center" type="button" onclick={() => removeUpload(upload.id)} - aria-label="Remove upload" + aria-label={$t('chat.removeUpload')} >

- What can I help you with? + {$t('chat.greeting')}

@@ -886,7 +887,7 @@ bind:selectedModel {sending} {workspace} - placeholder="Ask anything about {workspaceName}..." + placeholder={$t('chat.placeholder', { name: workspaceName })} onsend={send} {queuedMessages} onqueuesendnow={handleQueueSendNow} @@ -963,7 +964,7 @@ autoScroll = true; scrollToBottom(); }} - aria-label="Scroll to bottom" + aria-label={$t('chat.scrollToBottom')} > + import { t } from '$lib/i18n'; interface Props { ontext: (text: string) => void; } @@ -15,7 +16,7 @@ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { - alert('Speech recognition not supported in this browser.'); + alert($t('chat.dictate.unsupported')); return; } recognition = new SpeechRecognition(); diff --git a/cptr/frontend/src/lib/components/chat/OutputEditView.svelte b/cptr/frontend/src/lib/components/chat/OutputEditView.svelte index 8b6af5b..0d1793e 100644 --- a/cptr/frontend/src/lib/components/chat/OutputEditView.svelte +++ b/cptr/frontend/src/lib/components/chat/OutputEditView.svelte @@ -1,5 +1,6 @@ @@ -252,7 +253,7 @@ updateMessageText(di.indices[0], (e.target as HTMLTextAreaElement).value); autoResize(e); }} - placeholder="Message text..." + placeholder={$t('chat.editMessagePlaceholder')} rows="1" > {:else if di.type === 'reasoning'} @@ -264,7 +265,7 @@ updateReasoningText(di.indices[0], (e.target as HTMLTextAreaElement).value); autoResize(e); }} - placeholder="Reasoning text..." + placeholder={$t('chat.editReasoningPlaceholder')} rows="1" > {:else if di.type === 'function_call'} diff --git a/cptr/frontend/src/lib/components/chat/PlusMenu.svelte b/cptr/frontend/src/lib/components/chat/PlusMenu.svelte index cb5e221..dcb671d 100644 --- a/cptr/frontend/src/lib/components/chat/PlusMenu.svelte +++ b/cptr/frontend/src/lib/components/chat/PlusMenu.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { toolApprovalMode, planMode, requestParams, type ToolApprovalMode } from '$lib/stores'; import { tooltip } from '$lib/tooltip'; + import { t } from '$lib/i18n'; import Icon from '../Icon.svelte'; import ToggleSwitch from '../common/ToggleSwitch.svelte'; @@ -20,14 +21,14 @@ let pos = $state<{ x: number; bottom: number }>({ x: -9999, bottom: -9999 }); let ready = $state(false); - const modes: { value: ToolApprovalMode; label: string; desc: string }[] = [ - { value: 'ask', label: 'Ask for approval', desc: 'Confirm each tool call before it runs' }, - { value: 'auto', label: 'Auto-approve', desc: 'Approve safe actions, ask for risky ones' }, - { value: 'full', label: 'Full access', desc: 'All tool calls run without confirmation' } - ]; + const modes: { value: ToolApprovalMode; label: string; desc: string }[] = $derived([ + { value: 'ask', label: $t('plusMenu.askApproval'), desc: $t('plusMenu.askApprovalDesc') }, + { value: 'auto', label: $t('plusMenu.autoApprove'), desc: $t('plusMenu.autoApproveDesc') }, + { value: 'full', label: $t('plusMenu.fullAccess'), desc: $t('plusMenu.fullAccessDesc') } + ]); const currentModeLabel = $derived( - modes.find((m) => m.value === $toolApprovalMode)?.label ?? 'Tool permissions' + modes.find((m) => m.value === $toolApprovalMode)?.label ?? $t('plusMenu.toolPermissions') ); // ── Request params state ──────────────────────── @@ -254,7 +255,7 @@ d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" /> - Attach files + {$t('plusMenu.attachFiles')}
@@ -297,7 +298,7 @@ - Plan mode + {$t('plusMenu.planMode')} planMode.set(v)} /> @@ -308,7 +309,7 @@ onclick={() => (tab = 'tools')} > - Tool permissions + {$t('plusMenu.toolPermissions')} {currentModeLabel} @@ -333,7 +334,7 @@ - Parameters + {$t('plusMenu.parameters')} {#if paramCount > 0} {paramCount} {/if} @@ -348,7 +349,7 @@ onclick={() => (tab = '')} > - Tool permissions + {$t('plusMenu.toolPermissions')}
@@ -387,20 +388,20 @@ onclick={() => (tab = '')} > - Parameters + {$t('plusMenu.parameters')}
{#if paramRows.length === 0} -

No parameters configured

+

{$t('plusMenu.noParams')}

{:else}
{#each paramRows as row, i}
removeParamRow(i)} class="shrink-0 text-gray-300 dark:text-gray-700 opacity-0 group-hover/row:opacity-100 hover:text-gray-500 dark:hover:text-gray-400 transition-colors duration-75" - aria-label="Remove parameter" + aria-label={$t('plusMenu.removeParam')} > @@ -435,7 +436,7 @@ disabled={!canAddParam} > - Add parameter + {$t('plusMenu.addParam')}
{/if} diff --git a/cptr/frontend/src/lib/components/chat/QueuedMessageItem.svelte b/cptr/frontend/src/lib/components/chat/QueuedMessageItem.svelte index 5e2c6a3..afb86cb 100644 --- a/cptr/frontend/src/lib/components/chat/QueuedMessageItem.svelte +++ b/cptr/frontend/src/lib/components/chat/QueuedMessageItem.svelte @@ -7,6 +7,8 @@ ondelete: (id: string) => void; } let { id, content, onsendnow, onedit, ondelete }: Props = $props(); + + import { t } from '$lib/i18n';
@@ -35,7 +37,7 @@
{$t('common.cancel')} {$t('chat.send')}
@@ -186,7 +187,7 @@ class="p-0.5 rounded text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-default transition-colors duration-100" disabled={siblingIndex === 0} onclick={() => onnavigate?.(-1)} - aria-label="Previous message" + aria-label={$t('chat.prevMessage')} > onnavigate?.(1)} - aria-label="Next message" + aria-label={$t('chat.nextMessage')} > {#if copied} { e.stopPropagation(); onmenu?.(e); }} - aria-label="Chat options" + aria-label={$t('a11y.chatOptions')} > diff --git a/cptr/frontend/src/lib/components/common/Pagination.svelte b/cptr/frontend/src/lib/components/common/Pagination.svelte index 80c5a71..48dc6ae 100644 --- a/cptr/frontend/src/lib/components/common/Pagination.svelte +++ b/cptr/frontend/src/lib/components/common/Pagination.svelte @@ -5,6 +5,7 @@ onpagechange: (page: number) => void; } let { page, totalPages, onpagechange }: Props = $props(); + import { t } from '$lib/i18n'; // Build page numbers with ellipsis for large page counts const pages = $derived.by((): (number | 'ellipsis')[] => { @@ -28,7 +29,7 @@ class="pagination-btn" disabled={page <= 1} onclick={() => onpagechange(page - 1)} - aria-label="Previous page" + aria-label={$t('a11y.prevPage')} > = totalPages} onclick={() => onpagechange(page + 1)} - aria-label="Next page" + aria-label={$t('a11y.nextPage')} >

Empty sheet

'; + if (rows.length === 0) return `

${$t('preview.emptySheet')}

`; const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0); const colLetter = (i: number) => { diff --git a/cptr/frontend/src/lib/components/preview/SqliteView.svelte b/cptr/frontend/src/lib/components/preview/SqliteView.svelte index a646cc1..476fa0c 100644 --- a/cptr/frontend/src/lib/components/preview/SqliteView.svelte +++ b/cptr/frontend/src/lib/components/preview/SqliteView.svelte @@ -2,6 +2,7 @@ import { onMount, onDestroy } from 'svelte'; import { fetchHandler } from '$lib/apis'; import Spinner from '$lib/components/common/Spinner.svelte'; + import { t } from '$lib/i18n'; interface Props { src: string; @@ -47,7 +48,7 @@ } } catch (e: any) { console.error('SQLite load error:', e); - error = e.message || 'Failed to load database.'; + error = e.message || $t('sqlite.loadError'); } finally { loading = false; } @@ -122,8 +123,8 @@ } function formatCell(val: unknown): string { - if (val === null) return 'NULL'; - if (val instanceof Uint8Array) return `[BLOB ${val.length}B]`; + if (val === null) return $t('sqlite.null'); + if (val instanceof Uint8Array) return $t('sqlite.blob', { size: val.length }); return String(val); } @@ -168,7 +169,7 @@ class:active={queryMode} onclick={() => { queryMode = true; - }}>SQL{$t('sqlite.sql')}
@@ -184,7 +185,7 @@ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') runQuery(); }} > - + {/if} @@ -219,12 +220,12 @@ {#if !queryMode && totalRows > PAGE_SIZE} {/if} diff --git a/cptr/frontend/src/lib/i18n/locales/de.json b/cptr/frontend/src/lib/i18n/locales/de.json index 964c9c4..bc5e2ae 100644 --- a/cptr/frontend/src/lib/i18n/locales/de.json +++ b/cptr/frontend/src/lib/i18n/locales/de.json @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. Alle Rechte vorbehalten.", "about.updateAvailable": "v{{version}} verfügbar", "connections.failedToUpdate": "Verbindung konnte nicht aktualisiert werden", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", + "keyboard.newFile": "Eine neue leere Datei öffnen", + "keyboard.newTerminal": "Eine neue Terminalsitzung öffnen", + "keyboard.newChat": "Einen neuen KI-Chat starten", + "keyboard.closeTab": "Den aktiven Tab schließen", + "keyboard.nextTab": "Zum nächsten Tab wechseln", + "keyboard.prevTab": "Zum vorherigen Tab wechseln", + "keyboard.quickOpen": "Dateien schnell suchen und öffnen", + "keyboard.openSettings": "Einstellungen öffnen", + "keyboard.toggleSplit": "Geteilte Ansicht umschalten", + "keyboard.toggleSidebar": "Seitenleiste ein-/ausblenden", + "automations.title": "Automatisierungen", + "automations.filter": "Filtern...", + "automations.noMatches": "Keine Treffer", + "automations.noAutomations": "Noch keine Automatisierungen", + "automations.create": "Automatisierung erstellen", "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "automations.active": "Aktiv", + "automations.paused": "Pausiert", + "automations.all": "Alle", + "automations.schedule": "Zeitplan", + "automations.model": "Modell", + "automations.nextRun": "Nächste Ausführung", + "automations.lastRun": "Letzte Ausführung", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", + "automations.enabled": "Aktiviert", + "automations.regenerate": "Neu generieren", + "automations.disable": "Deaktivieren", + "automations.enable": "Aktivieren", + "automations.copied": "Kopiert!", + "automations.copy": "Kopieren", "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.runs": "Ausführungen", + "automations.noRuns": "Noch keine Ausführungen", + "automations.viewChat": "Chat ansehen →", + "automations.deleted": "Gelöscht", + "automations.webhookEnabled": "Webhook aktiviert", + "automations.webhookDisabled": "Webhook deaktiviert", + "automations.failedToLoad": "Automatisierungen konnten nicht geladen werden", + "automations.failedToLoadOne": "Automatisierung konnte nicht geladen werden", + "automations.failedToToggle": "Umschalten fehlgeschlagen", + "automations.failedToDelete": "Löschen fehlgeschlagen", + "automations.failedToRun": "Ausführung fehlgeschlagen", + "automations.failedToGenerateWebhook": "Webhook konnte nicht generiert werden", + "automations.failedToRevokeWebhook": "Webhook konnte nicht widerrufen werden", + "automations.triggered": "\"{{name}}\" ausgelöst", + "automations.deleteConfirm": "\"{{name}}\" löschen?", + "automations.runNow": "Jetzt ausführen", + "automations.newAutomation": "Neue Automatisierung", + "automations.toggleSidebar": "Seitenleiste umschalten", + "automationModal.titlePlaceholder": "Titel der Automatisierung", + "automationModal.instructions": "Anweisungen", + "automationModal.promptPlaceholder": "Prompt hier eingeben...", + "automationModal.selectWorkspace": "Arbeitsbereich auswählen", + "automationModal.cancel": "Abbrechen", + "automationModal.saving": "Speichern...", + "automationModal.save": "Speichern", + "automationModal.createBtn": "Erstellen", + "automationModal.failedToSave": "Automatisierung konnte nicht gespeichert werden", + "common.cancel": "Abbrechen", + "common.save": "Speichern", + "common.edit": "Bearbeiten", + "common.copy": "Kopieren", + "common.remove": "Entfernen", + "common.close": "Schließen", + "common.dismiss": "Verwerfen", + "common.loading": "Laden", + "common.downloadCsv": "CSV herunterladen", + "chat.tool.readFile": "Lesen {{path}}", + "chat.tool.readFileRange": "Lesen {{path}} Z{{range}}", + "chat.tool.editFile": "Bearbeiten {{path}}", + "chat.tool.multiEditFile": "Mehrfachbearbeitung {{path}}", + "chat.tool.createFile": "Erstellen {{path}}", + "chat.tool.writeFile": "Schreiben {{path}}", + "chat.tool.listDirectory": "Auflisten {{path}}", + "chat.tool.listDirectoryRecursive": "Auflisten {{path}} (rekursiv)", + "chat.tool.searchFiles": "Suche \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " in {{include}}", + "chat.tool.backgroundCommand": "Hintergrund: {{command}}", + "chat.tool.checkTask": "Aufgabe prüfen {{id}}", + "chat.tool.killTask": "Aufgabe beenden {{id}}", + "chat.tool.webSearch": "Websuche: \"{{query}}\"", + "chat.tool.fetchUrl": "Abrufen {{hostname}}", + "chat.tool.fetchUrlFallback": "URL abrufen", + "chat.saveAs": "Speichern unter", + "chat.send": "Senden", + "chat.artifact": "Artefakt", + "chat.exploring": "Wird erkundet", + "chat.explored": "Erkundet", + "chat.allow": "Erlauben", + "chat.deny": "Ablehnen", + "chat.toolInput": "Eingabe", + "chat.toolOutput": "Ausgabe", + "chat.toolLines": "Zeilen {{start}}–{{end}}", + "chat.totalChars": "{{count}} Zeichen insgesamt", + "chat.placeholder": "Frag alles über {{name}}...", + "chat.prevResponse": "Vorherige Antwort", + "chat.nextResponse": "Nächste Antwort", + "chat.editResponse": "Antwort bearbeiten", + "chat.copyResponse": "Antwort kopieren", + "chat.regenerateResponse": "Antwort neu generieren", + "chat.usageInfo": "Nutzungsinfo", + "chat.toggleToolCalls": "Tool-Aufrufe umschalten", + "chat.scrollToBottom": "Nach unten scrollen", + "chat.removeUpload": "Upload entfernen", + "chat.sendNow": "Jetzt senden", + "chat.prevMessage": "Vorherige Nachricht", + "chat.nextMessage": "Nächste Nachricht", + "chat.editMessage": "Nachricht bearbeiten", + "chat.copyMessage": "Nachricht kopieren", + "chat.editMessagePlaceholder": "Nachrichtentext...", + "chat.editReasoningPlaceholder": "Begründungstext...", + "plusMenu.askApproval": "Um Genehmigung bitten", + "plusMenu.askApprovalDesc": "Jeden Tool-Aufruf vor der Ausführung bestätigen", + "plusMenu.autoApprove": "Automatisch genehmigen", + "plusMenu.autoApproveDesc": "Sichere Aktionen genehmigen, bei riskanten nachfragen", + "plusMenu.fullAccess": "Vollzugriff", + "plusMenu.fullAccessDesc": "Alle Tool-Aufrufe ohne Bestätigung ausführen", + "plusMenu.attachFiles": "Dateien anhängen", + "plusMenu.capture": "Aufnehmen", + "plusMenu.planMode": "Planungsmodus", + "plusMenu.toolPermissions": "Tool-Berechtigungen", + "plusMenu.parameters": "Parameter", + "plusMenu.noParams": "Keine Parameter konfiguriert", + "plusMenu.paramKey": "Schlüssel", + "plusMenu.paramValue": "Wert", + "plusMenu.removeParam": "Parameter entfernen", + "plusMenu.addParam": "Parameter hinzufügen", + "voiceMemo.microphoneError": "Mikrofon konnte nicht zugegriffen werden", + "voiceMemo.uploadFailed": "Audio lokal gespeichert. Server-Upload fehlgeschlagen.", + "voiceMemo.sttNotConfigured": "STT nicht konfiguriert. Einrichten unter Einstellungen → Audio.", + "voiceMemo.writeFailed": "Transkript konnte nicht geschrieben werden. Audio ist sicher.", + "voiceMemo.done": "Fertig →", + "voiceMemo.processing": "Verarbeitung…", + "voiceMemo.filename": "Dateiname", + "voiceMemo.filenamePlaceholder": "aufnahme-name", + "voiceMemo.saved": "Gespeichert ✓", + "admin.audio.title": "Audio", + "admin.audio.voiceMemos": "Sprachnotizen", + "admin.audio.enableVoiceMemos": "Sprachnotizen aktivieren", + "admin.audio.voiceMemosHint": "Sprachnotizen über das \"+\"-Menü aufnehmen.", + "admin.audio.autoTranscribe": "Automatisch transkribieren", + "admin.audio.transcribeOnHint": "Aufnahmen werden per STT in Markdown transkribiert.", + "admin.audio.transcribeOffHint": "Aufnahmen werden nur als Audio gespeichert.", + "admin.audio.recordingQuality": "Aufnahmequalität", + "admin.audio.qualityHigh": "Hoch (128kbps)", + "admin.audio.qualityMedium": "Mittel (64kbps)", + "admin.audio.qualityLow": "Niedrig (32kbps)", + "admin.audio.qualityHintHigh": "Beste Qualität, größere Dateien.", + "admin.audio.qualityHintMedium": "Ausgewogene Qualität und Größe.", + "admin.audio.qualityHintLow": "Kleinste Dateien, optimiert für Sprache.", + "admin.audio.stt": "Sprache-zu-Text", + "admin.audio.sttHint": "Kompatibel mit OpenAIs Audio-/Transkriptions-API.", + "admin.audio.saveFailed": "Audioeinstellungen konnten nicht gespeichert werden", + "sqlite.loadError": "Datenbank konnte nicht geladen werden.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Ausführen ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} von {{total}}", + "sqlite.prev": "← Zurück", + "sqlite.next": "Weiter →", + "preview.htmlTitle": "HTML-Vorschau", + "preview.emptySheet": "Leeres Blatt", + "port.cannotConnect": "Verbindung nicht möglich", + "connections.optional": "Optional", + "connections.leaveBlankPlaceholder": "•••••••• (leer lassen zum Beibehalten)", + "a11y.dismissNotification": "Benachrichtigung verwerfen", + "a11y.chatOptions": "Chat-Optionen", + "a11y.prevPage": "Vorherige Seite", + "a11y.nextPage": "Nächste Seite", + "a11y.splitEditor": "Editor teilen", + "a11y.closePane": "Bereich schließen", + "a11y.refreshChanges": "Änderungen aktualisieren", + "keyboard.clickToRebind": "Klicken zum Neuzuweisen", + "bar.splitRight": "Rechts teilen", + "bar.splitDown": "Unten teilen", + "bar.voiceMemo": "Sprachnotiz", + "bar.files": "Dateien", + "messaging.noBots": "Keine Messaging-Bots konfiguriert.", + "messaging.failedToLoad": "Bots konnten nicht geladen werden", + "messaging.failedToToggle": "Bot konnte nicht umgeschaltet werden", + "messaging.failedToDelete": "Bot konnte nicht gelöscht werden", + "messaging.editBot": "Bot bearbeiten", + "messaging.addBot": "Bot hinzufügen", + "messaging.name": "Name", + "messaging.platform": "Plattform", + "messaging.token": "Token", + "messaging.tokenKeep": "Leer lassen zum Beibehalten", + "messaging.tokenPaste": "Token einfügen", + "messaging.verify": "Überprüfen", + "messaging.allowedSenders": "Erlaubte Absender", + "messaging.allowedSendersHint": "Kommagetrennte Benutzer-IDs. Leer lassen für alle.", + "messaging.save": "Speichern →", + "messaging.add": "Hinzufügen →", + "messaging.failedToSave": "Speichern fehlgeschlagen", + "messaging.hint.telegram": "Bot über @BotFather erstellen", + "messaging.hint.discord": "Bot im Developer Portal erstellen", + "messaging.hint.slack": "Bot Token | App Token (pipe-getrennt)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (pipe-getrennt)", + "messaging.hint.signal": "signal-cli URL | Telefonnummer (pipe-getrennt)", + "admin.gateway.keys": "Schlüssel", + "admin.gateway.copy": "Kopieren", + "admin.gateway.baseUrl": "Basis-URL:", + "admin.gateway.apiKey": "API-Schlüssel:", + "admin.gateway.headers": "Header:", + "admin.messaging": "Nachrichten", + "admin.gateway.tab": "Gateway", + "keyboard.action.newFile": "Neue Datei", + "keyboard.action.newTerminal": "Neues Terminal", + "keyboard.action.newChat": "Neuer Chat", + "keyboard.action.closeTab": "Tab schließen", + "keyboard.action.nextTab": "Nächster Tab", + "keyboard.action.prevTab": "Vorheriger Tab", + "keyboard.action.quickOpen": "Schnell öffnen", + "keyboard.action.searchAll": "Suche", + "keyboard.action.openSettings": "Einstellungen öffnen", + "keyboard.action.toggleSplit": "Teilung umschalten", + "keyboard.action.toggleSidebar": "Seitenleiste umschalten", + "keyboard.action.voiceMemo": "Sprachnotiz", + "keyboard.conflict": "Auch belegt von {{action}}", + "chat.history.justNow": "Gerade eben", + "chat.history.minutesAgo": "vor {{count}}min", + "chat.history.hoursAgo": "vor {{count}}h", + "chat.history.daysAgo": "vor {{count}}T", + "chat.history.title": "Titel", + "chat.history.updated": "Aktualisiert", + "chat.history.delete": "Löschen", + "chat.edit.text": "Text", + "chat.edit.thought": "Gedanke", + "chat.edit.tool": "Werkzeug", + "chat.edit.item": "Element", + "chat.dictate.unsupported": "Spracherkennung wird in diesem Browser nicht unterstützt.", + "chat.fallbackTitle": "Chat", + "admin.gateway.copied": "In die Zwischenablage kopiert", + "admin.gateway.createError": "Schlüssel konnte nicht erstellt werden", + "admin.gateway.createKey": "Schlüssel erstellen", + "admin.gateway.deleteError": "Schlüssel konnte nicht gelöscht werden", + "admin.gateway.description": "Generieren Sie API-Schlüssel, um Open WebUI oder einen OpenAI-kompatiblen Client mit Ihren Arbeitsbereichen zu verbinden.", + "admin.gateway.howToConnect": "Von Open WebUI verbinden", + "admin.gateway.keyCreated": "API-Schlüssel erstellt", + "admin.gateway.keyDeleted": "API-Schlüssel gelöscht", + "admin.gateway.keyNamePlaceholder": "Schlüsselname (z.B. open-webui)", + "admin.gateway.keyWarning": "Dieser Schlüssel wird nur einmal angezeigt. Speichern Sie ihn sicher.", + "admin.gateway.loadError": "API-Schlüssel konnten nicht geladen werden", + "admin.gateway.model": "Antwortmodell", + "admin.gateway.modelDescription": "Gateway-Clients wählen einen Arbeitsbereich; cptr verwendet dieses Modell für die Antwort.", + "admin.gateway.modelFallback": "Arbeitsbereich-/Standardmodell verwenden", + "admin.gateway.modelSaveError": "Gateway-Modell konnte nicht gespeichert werden", + "admin.gateway.newKey": "Neuer API-Schlüssel erstellt. Jetzt kopieren", + "admin.gateway.noKeys": "Noch keine API-Schlüssel", + "admin.gateway.title": "API-Gateway", + "chat.greeting": "Wie kann ich Ihnen helfen?", + + "admin.subagents": "Sub-Agenten", + "admin.subagentsEnabled": "Sub-Agenten aktivieren", + "admin.subagentsHint": "Ermöglicht der KI, Aufgaben an Sub-Agenten zu delegieren. Jeder Sub-Agent erstellt einen echten Chat mit vollem Werkzeugzugriff. Verwendet zusätzliche LLM-Aufrufe.", + "admin.subagentsMaxConcurrent": "Max. gleichzeitig", + "admin.subagentsMaxConcurrentHint": "gleichzeitige Sub-Agenten", + "admin.subagentsMaxIterations": "Max. Iterationen", + "admin.subagentsMaxIterationsHint": "Werkzeugschleifen pro Sub-Agent", + "admin.subagentsMaxOutput": "Max. Ausgabe", + "admin.subagentsSystemPrompt": "System-Prompt", + "admin.subagentsSystemPromptPlaceholder": "Du bist ein Sub-Agent...", + "admin.subagentsSystemPromptHint": "Leer lassen für den integrierten Standard." } diff --git a/cptr/frontend/src/lib/i18n/locales/en.json b/cptr/frontend/src/lib/i18n/locales/en.json index f0b91e0..fc3c248 100644 --- a/cptr/frontend/src/lib/i18n/locales/en.json +++ b/cptr/frontend/src/lib/i18n/locales/en.json @@ -322,7 +322,6 @@ "general.interruptDesc": "Sending a message cancels the current generation.", "settings.keyboard": "Keyboard", - "settings.configuration": "Configuration", "keyboard.title": "Keyboard Shortcuts", "keyboard.resetDefaults": "Reset defaults", @@ -452,5 +451,252 @@ "messaging.namePlaceholder": "Bot name...", "messaging.workspacePlaceholder": "Workspace path (e.g. /Users/you/project)", "messaging.modelPlaceholder": "Model ID (e.g. openrouter/claude-sonnet-4-20250514)", - "messaging.sendersPlaceholder": "Allowed user IDs (comma-separated, leave empty for all)" + "messaging.sendersPlaceholder": "Allowed user IDs (comma-separated, leave empty for all)", + + "common.cancel": "Cancel", + "common.save": "Save", + "common.edit": "Edit", + "common.copy": "Copy", + "common.remove": "Remove", + "common.close": "Close", + "common.dismiss": "Dismiss", + "common.loading": "Loading", + "common.downloadCsv": "Download CSV", + + "chat.tool.readFile": "Read {{path}}", + "chat.tool.readFileRange": "Read {{path}} L{{range}}", + "chat.tool.editFile": "Edit {{path}}", + "chat.tool.multiEditFile": "Multi-edit {{path}}", + "chat.tool.createFile": "Create {{path}}", + "chat.tool.writeFile": "Write {{path}}", + "chat.tool.listDirectory": "List {{path}}", + "chat.tool.listDirectoryRecursive": "List {{path}} (recursive)", + "chat.tool.searchFiles": "Search \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " in {{include}}", + "chat.tool.backgroundCommand": "Background: {{command}}", + "chat.tool.checkTask": "Check task {{id}}", + "chat.tool.killTask": "Kill task {{id}}", + "chat.tool.webSearch": "Search web: \"{{query}}\"", + "chat.tool.fetchUrl": "Fetch {{hostname}}", + "chat.tool.fetchUrlFallback": "Fetch URL", + "chat.saveAs": "Save As", + "chat.send": "Send", + "chat.artifact": "Artifact", + "chat.exploring": "Exploring", + "chat.explored": "Explored", + "chat.allow": "Allow", + "chat.deny": "Deny", + "chat.toolInput": "Input", + "chat.toolOutput": "Output", + "chat.toolLines": "Lines {{start}}–{{end}}", + "chat.totalChars": "{{count}} total characters", + "chat.placeholder": "Ask anything about {{name}}...", + "chat.prevResponse": "Previous response", + "chat.nextResponse": "Next response", + "chat.editResponse": "Edit response", + "chat.copyResponse": "Copy response", + "chat.regenerateResponse": "Regenerate response", + "chat.usageInfo": "Usage info", + "chat.toggleToolCalls": "Toggle tool calls", + "chat.scrollToBottom": "Scroll to bottom", + "chat.removeUpload": "Remove upload", + "chat.sendNow": "Send now", + "chat.prevMessage": "Previous message", + "chat.nextMessage": "Next message", + "chat.editMessage": "Edit message", + "chat.copyMessage": "Copy message", + "chat.editMessagePlaceholder": "Message text...", + "chat.editReasoningPlaceholder": "Reasoning text...", + + "plusMenu.askApproval": "Ask for approval", + "plusMenu.askApprovalDesc": "Confirm each tool call before it runs", + "plusMenu.autoApprove": "Auto-approve", + "plusMenu.autoApproveDesc": "Approve safe actions, ask for risky ones", + "plusMenu.fullAccess": "Full access", + "plusMenu.fullAccessDesc": "All tool calls run without confirmation", + "plusMenu.attachFiles": "Attach files", + "plusMenu.capture": "Capture", + "plusMenu.planMode": "Plan mode", + "plusMenu.toolPermissions": "Tool permissions", + "plusMenu.parameters": "Parameters", + "plusMenu.noParams": "No parameters configured", + "plusMenu.paramKey": "key", + "plusMenu.paramValue": "value", + "plusMenu.removeParam": "Remove parameter", + "plusMenu.addParam": "Add parameter", + + "voiceMemo.microphoneError": "Could not access microphone", + "voiceMemo.uploadFailed": "Audio saved locally. Server upload failed.", + "voiceMemo.sttNotConfigured": "STT not configured. Set up in Settings → Audio.", + "voiceMemo.writeFailed": "Failed to write transcript. Audio is safe.", + "voiceMemo.done": "Done →", + "voiceMemo.processing": "Processing…", + "voiceMemo.filename": "Filename", + "voiceMemo.filenamePlaceholder": "recording-name", + "voiceMemo.saved": "Saved ✓", + + "admin.audio.title": "Audio", + "admin.audio.voiceMemos": "Voice Memos", + "admin.audio.enableVoiceMemos": "Enable Voice Memos", + "admin.audio.voiceMemosHint": "Record voice memos from the \"+\" menu.", + "admin.audio.autoTranscribe": "Auto-transcribe", + "admin.audio.transcribeOnHint": "Recordings are transcribed to markdown via STT.", + "admin.audio.transcribeOffHint": "Recordings are saved as audio only.", + "admin.audio.recordingQuality": "Recording quality", + "admin.audio.qualityHigh": "High (128kbps)", + "admin.audio.qualityMedium": "Medium (64kbps)", + "admin.audio.qualityLow": "Low (32kbps)", + "admin.audio.qualityHintHigh": "Best quality, larger files.", + "admin.audio.qualityHintMedium": "Balanced quality and size.", + "admin.audio.qualityHintLow": "Smallest files, optimized for speech.", + "admin.audio.stt": "Speech-to-Text", + "admin.audio.sttHint": "Compatible with OpenAI's audio/transcriptions API.", + "admin.audio.saveFailed": "Failed to save audio settings", + + "sqlite.loadError": "Failed to load database.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Run ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} of {{total}}", + "sqlite.prev": "← Prev", + "sqlite.next": "Next →", + + "preview.htmlTitle": "HTML Preview", + "preview.emptySheet": "Empty sheet", + + "port.cannotConnect": "Cannot connect", + + "connections.optional": "Optional", + "connections.leaveBlankPlaceholder": "•••••••• (leave blank to keep)", + + "a11y.dismissNotification": "Dismiss notification", + "a11y.chatOptions": "Chat options", + "a11y.prevPage": "Previous page", + "a11y.nextPage": "Next page", + "a11y.splitEditor": "Split Editor", + "a11y.closePane": "Close pane", + "a11y.refreshChanges": "Refresh changes", + + "keyboard.clickToRebind": "Click to rebind", + + "bar.splitRight": "Split Right", + "bar.splitDown": "Split Down", + "bar.voiceMemo": "Voice Memo", + "bar.files": "Files", + + "messaging.noBots": "No messaging bots configured.", + "messaging.failedToLoad": "Failed to load bots", + "messaging.failedToToggle": "Failed to toggle bot", + "messaging.failedToDelete": "Failed to delete bot", + "messaging.editBot": "Edit Bot", + "messaging.addBot": "Add Bot", + "messaging.name": "Name", + "messaging.platform": "Platform", + "messaging.token": "Token", + "messaging.tokenKeep": "Leave empty to keep current", + "messaging.tokenPaste": "Paste token", + "messaging.verify": "Verify", + "messaging.allowedSenders": "Allowed senders", + "messaging.allowedSendersHint": "Comma-separated user IDs. Leave empty to allow all.", + "messaging.save": "Save →", + "messaging.add": "Add →", + "messaging.failedToSave": "Failed to save", + + "messaging.hint.telegram": "Create a bot via @BotFather", + "messaging.hint.discord": "Create a bot in the Developer Portal", + "messaging.hint.slack": "Bot Token | App Token (pipe-separated)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (pipe-separated)", + "messaging.hint.signal": "signal-cli URL | Phone Number (pipe-separated)", + + "admin.gateway.keys": "Keys", + "admin.gateway.copy": "Copy", + "admin.gateway.baseUrl": "Base URL:", + "admin.gateway.apiKey": "API Key:", + "admin.gateway.headers": "Headers:", + + "keyboard.action.newFile": "New File", + "keyboard.action.newTerminal": "New Terminal", + "keyboard.action.newChat": "New Chat", + "keyboard.action.closeTab": "Close Tab", + "keyboard.action.nextTab": "Next Tab", + "keyboard.action.prevTab": "Previous Tab", + "keyboard.action.quickOpen": "Quick Open", + "keyboard.action.searchAll": "Search", + "keyboard.action.openSettings": "Open Settings", + "keyboard.action.toggleSplit": "Toggle Split", + "keyboard.action.toggleSidebar": "Toggle Sidebar", + "keyboard.action.voiceMemo": "Voice Memo", + "keyboard.conflict": "Also bound to {{action}}", + + "chat.history.justNow": "Just now", + "chat.history.minutesAgo": "{{count}}m ago", + "chat.history.hoursAgo": "{{count}}h ago", + "chat.history.daysAgo": "{{count}}d ago", + "chat.history.title": "Title", + "chat.history.updated": "Updated", + "chat.history.delete": "Delete", + + "chat.edit.text": "Text", + "chat.edit.thought": "Thought", + "chat.edit.tool": "Tool", + "chat.edit.item": "Item", + + "chat.dictate.unsupported": "Speech recognition not supported in this browser.", + "chat.fallbackTitle": "Chat", + "chat.greeting": "What can I help you with?", + + "admin.toolServers": "Tool Servers", + + "toolServers.title": "Tool Servers", + "toolServers.empty": "No tool servers configured", + "toolServers.add": "Add Server", + "toolServers.edit": "Edit Server", + "toolServers.id": "ID", + "toolServers.name": "Name", + "toolServers.type": "Type", + "toolServers.namePlaceholder": "My MCP Server", + "toolServers.url": "URL", + "toolServers.specPath": "Spec path", + "toolServers.fieldsRequired": "ID and URL are required", + "toolServers.idInvalid": "ID must be lowercase letters, numbers, and underscores only", + "toolServers.auth": "Auth", + "toolServers.authNone": "None", + "toolServers.authBearer": "Bearer", + "toolServers.apiKey": "API Key", + "toolServers.description": "Description", + "toolServers.descriptionPlaceholder": "What does this server do?", + "toolServers.headers": "Headers", + "toolServers.headersHint": "Additional HTTP headers as JSON.", + "toolServers.headersInvalid": "Headers must be a valid JSON object", + "toolServers.verify": "Verify", + "toolServers.connected": "Connected", + "toolServers.toolsFound": "tools found", + "toolServers.delete": "Delete", + "toolServers.loadError": "Failed to load tool servers", + "toolServers.urlRequired": "URL is required", + "toolServers.created": "Tool server added", + "toolServers.updated": "Tool server updated", + "toolServers.deleted": "Tool server deleted", + "toolServers.saveFailed": "Failed to save tool server", + "toolServers.deleteFailed": "Failed to delete tool server", + + "admin.subagents": "Sub-agents", + "admin.subagentsEnabled": "Enable sub-agents", + "admin.subagentsHint": "Allow the AI to delegate tasks to sub-agents. Each sub-agent creates a real chat with full tool access. Uses additional LLM calls.", + "admin.subagentsMaxConcurrent": "Max concurrent", + "admin.subagentsMaxConcurrentHint": "simultaneous sub-agents", + "admin.subagentsMaxIterations": "Max iterations", + "admin.subagentsMaxIterationsHint": "tool loops per sub-agent", + "admin.subagentsMaxOutput": "Max output", + "admin.subagentsSystemPrompt": "System prompt", + "admin.subagentsSystemPromptPlaceholder": "You are a sub-agent...", + "admin.subagentsSystemPromptHint": "Leave empty for the built-in default.", + + "admin.contextCompaction": "Context compaction", + "admin.compactTokenThreshold": "Token threshold", + "admin.compactTokenThresholdUnit": "tokens", + "admin.compactTokenThresholdHint": "Older messages are summarized when estimated context exceeds this limit. Default: 80,000." } + diff --git a/cptr/frontend/src/lib/i18n/locales/es.json b/cptr/frontend/src/lib/i18n/locales/es.json index 274bb79..5e69a66 100644 --- a/cptr/frontend/src/lib/i18n/locales/es.json +++ b/cptr/frontend/src/lib/i18n/locales/es.json @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. Todos los derechos reservados.", "about.updateAvailable": "v{{version}} disponible", "connections.failedToUpdate": "Error al actualizar conexión", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "keyboard.newFile": "Abrir un archivo nuevo vacío", + "keyboard.newTerminal": "Abrir una nueva sesión de terminal", + "keyboard.newChat": "Iniciar un nuevo chat de IA", + "keyboard.closeTab": "Cerrar la pestaña activa", + "keyboard.nextTab": "Cambiar a la siguiente pestaña", + "keyboard.prevTab": "Cambiar a la pestaña anterior", + "keyboard.quickOpen": "Buscar y abrir archivos rápidamente", + "keyboard.openSettings": "Abrir el panel de ajustes", + "keyboard.toggleSplit": "Alternar vista dividida del editor", + "keyboard.toggleSidebar": "Mostrar u ocultar la barra lateral", + "automations.title": "Automatizaciones", + "automations.filter": "Filtrar...", + "automations.noMatches": "Sin resultados", + "automations.noAutomations": "Aún no hay automatizaciones", + "automations.create": "Crear automatización", + "automations.status": "Estado", + "automations.active": "Activa", + "automations.paused": "Pausada", + "automations.all": "Todas", + "automations.schedule": "Programación", + "automations.model": "Modelo", + "automations.nextRun": "Próxima ejecución", + "automations.lastRun": "Última ejecución", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", + "automations.enabled": "Habilitado", + "automations.regenerate": "Regenerar", + "automations.disable": "Desactivar", + "automations.enable": "Activar", + "automations.copied": "¡Copiado!", + "automations.copy": "Copiar", "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.runs": "Ejecuciones", + "automations.noRuns": "Aún no hay ejecuciones", + "automations.viewChat": "ver chat →", + "automations.deleted": "Eliminada", + "automations.webhookEnabled": "Webhook habilitado", + "automations.webhookDisabled": "Webhook deshabilitado", + "automations.failedToLoad": "Error al cargar las automatizaciones", + "automations.failedToLoadOne": "Error al cargar la automatización", + "automations.failedToToggle": "Error al cambiar", + "automations.failedToDelete": "Error al eliminar", + "automations.failedToRun": "Error al ejecutar", + "automations.failedToGenerateWebhook": "Error al generar webhook", + "automations.failedToRevokeWebhook": "Error al revocar webhook", + "automations.triggered": "\"{{name}}\" ejecutada", + "automations.deleteConfirm": "¿Eliminar \"{{name}}\"?", + "automations.runNow": "Ejecutar ahora", + "automations.newAutomation": "Nueva automatización", + "automations.toggleSidebar": "Alternar barra lateral", + "automationModal.titlePlaceholder": "Título de la automatización", + "automationModal.instructions": "Instrucciones", + "automationModal.promptPlaceholder": "Ingrese el prompt aquí...", + "automationModal.selectWorkspace": "Seleccionar espacio de trabajo", + "automationModal.cancel": "Cancelar", + "automationModal.saving": "Guardando...", + "automationModal.save": "Guardar", + "automationModal.createBtn": "Crear", + "automationModal.failedToSave": "No se pudo guardar la automatización", + "common.cancel": "Cancelar", + "common.save": "Guardar", + "common.edit": "Editar", + "common.copy": "Copiar", + "common.remove": "Eliminar", + "common.close": "Cerrar", + "common.dismiss": "Descartar", + "common.loading": "Cargando", + "common.downloadCsv": "Descargar CSV", + "chat.tool.readFile": "Leer {{path}}", + "chat.tool.readFileRange": "Leer {{path}} L{{range}}", + "chat.tool.editFile": "Editar {{path}}", + "chat.tool.multiEditFile": "Edición múltiple {{path}}", + "chat.tool.createFile": "Crear {{path}}", + "chat.tool.writeFile": "Escribir {{path}}", + "chat.tool.listDirectory": "Listar {{path}}", + "chat.tool.listDirectoryRecursive": "Listar {{path}} (recursivo)", + "chat.tool.searchFiles": "Buscar \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " en {{include}}", + "chat.tool.backgroundCommand": "Segundo plano: {{command}}", + "chat.tool.checkTask": "Verificar tarea {{id}}", + "chat.tool.killTask": "Terminar tarea {{id}}", + "chat.tool.webSearch": "Buscar en web: \"{{query}}\"", + "chat.tool.fetchUrl": "Obtener {{hostname}}", + "chat.tool.fetchUrlFallback": "Obtener URL", + "chat.saveAs": "Guardar como", + "chat.send": "Enviar", + "chat.artifact": "Artefacto", + "chat.exploring": "Explorando", + "chat.explored": "Explorado", + "chat.allow": "Permitir", + "chat.deny": "Denegar", + "chat.toolInput": "Entrada", + "chat.toolOutput": "Salida", + "chat.toolLines": "Líneas {{start}}–{{end}}", + "chat.totalChars": "{{count}} caracteres en total", + "chat.placeholder": "Pregunta cualquier cosa sobre {{name}}...", + "chat.prevResponse": "Respuesta anterior", + "chat.nextResponse": "Siguiente respuesta", + "chat.editResponse": "Editar respuesta", + "chat.copyResponse": "Copiar respuesta", + "chat.regenerateResponse": "Regenerar respuesta", + "chat.usageInfo": "Info de uso", + "chat.toggleToolCalls": "Alternar llamadas a herramientas", + "chat.scrollToBottom": "Desplazar al final", + "chat.removeUpload": "Eliminar archivo subido", + "chat.sendNow": "Enviar ahora", + "chat.prevMessage": "Mensaje anterior", + "chat.nextMessage": "Siguiente mensaje", + "chat.editMessage": "Editar mensaje", + "chat.copyMessage": "Copiar mensaje", + "chat.editMessagePlaceholder": "Texto del mensaje...", + "chat.editReasoningPlaceholder": "Texto del razonamiento...", + "plusMenu.askApproval": "Pedir aprobación", + "plusMenu.askApprovalDesc": "Confirmar cada llamada a herramienta antes de ejecutarla", + "plusMenu.autoApprove": "Aprobar automáticamente", + "plusMenu.autoApproveDesc": "Aprobar acciones seguras, preguntar por las riesgosas", + "plusMenu.fullAccess": "Acceso completo", + "plusMenu.fullAccessDesc": "Todas las llamadas se ejecutan sin confirmación", + "plusMenu.attachFiles": "Adjuntar archivos", + "plusMenu.capture": "Capturar", + "plusMenu.planMode": "Modo plan", + "plusMenu.toolPermissions": "Permisos de herramientas", + "plusMenu.parameters": "Parámetros", + "plusMenu.noParams": "No hay parámetros configurados", + "plusMenu.paramKey": "clave", + "plusMenu.paramValue": "valor", + "plusMenu.removeParam": "Eliminar parámetro", + "plusMenu.addParam": "Añadir parámetro", + "voiceMemo.microphoneError": "No se pudo acceder al micrófono", + "voiceMemo.uploadFailed": "Audio guardado localmente. La subida al servidor falló.", + "voiceMemo.sttNotConfigured": "STT no configurado. Configúralo en Ajustes → Audio.", + "voiceMemo.writeFailed": "No se pudo escribir la transcripción. El audio está seguro.", + "voiceMemo.done": "Listo →", + "voiceMemo.processing": "Procesando…", + "voiceMemo.filename": "Nombre de archivo", + "voiceMemo.filenamePlaceholder": "nombre-grabación", + "voiceMemo.saved": "Guardado ✓", + "admin.audio.title": "Audio", + "admin.audio.voiceMemos": "Notas de voz", + "admin.audio.enableVoiceMemos": "Activar notas de voz", + "admin.audio.voiceMemosHint": "Graba notas de voz desde el menú \"+\".", + "admin.audio.autoTranscribe": "Transcripción automática", + "admin.audio.transcribeOnHint": "Las grabaciones se transcriben a markdown mediante STT.", + "admin.audio.transcribeOffHint": "Las grabaciones se guardan solo como audio.", + "admin.audio.recordingQuality": "Calidad de grabación", + "admin.audio.qualityHigh": "Alta (128kbps)", + "admin.audio.qualityMedium": "Media (64kbps)", + "admin.audio.qualityLow": "Baja (32kbps)", + "admin.audio.qualityHintHigh": "Mejor calidad, archivos más grandes.", + "admin.audio.qualityHintMedium": "Calidad y tamaño equilibrados.", + "admin.audio.qualityHintLow": "Archivos más pequeños, optimizado para voz.", + "admin.audio.stt": "Voz a texto", + "admin.audio.sttHint": "Compatible con la API de audio/transcripciones de OpenAI.", + "admin.audio.saveFailed": "No se pudieron guardar los ajustes de audio", + "sqlite.loadError": "No se pudo cargar la base de datos.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Ejecutar ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} de {{total}}", + "sqlite.prev": "← Anterior", + "sqlite.next": "Siguiente →", + "preview.htmlTitle": "Vista previa HTML", + "preview.emptySheet": "Hoja vacía", + "port.cannotConnect": "No se puede conectar", + "connections.optional": "Opcional", + "connections.leaveBlankPlaceholder": "•••••••• (dejar en blanco para mantener)", + "a11y.dismissNotification": "Descartar notificación", + "a11y.chatOptions": "Opciones de chat", + "a11y.prevPage": "Página anterior", + "a11y.nextPage": "Página siguiente", + "a11y.splitEditor": "Dividir editor", + "a11y.closePane": "Cerrar panel", + "a11y.refreshChanges": "Actualizar cambios", + "keyboard.clickToRebind": "Clic para reasignar", + "bar.splitRight": "Dividir a la derecha", + "bar.splitDown": "Dividir abajo", + "bar.voiceMemo": "Nota de voz", + "bar.files": "Archivos", + "messaging.noBots": "No hay bots de mensajería configurados.", + "messaging.failedToLoad": "Error al cargar los bots", + "messaging.failedToToggle": "Error al cambiar el estado del bot", + "messaging.failedToDelete": "Error al eliminar el bot", + "messaging.editBot": "Editar Bot", + "messaging.addBot": "Añadir Bot", + "messaging.name": "Nombre", + "messaging.platform": "Plataforma", + "messaging.token": "Token", + "messaging.tokenKeep": "Dejar vacío para mantener", + "messaging.tokenPaste": "Pegar token", + "messaging.verify": "Verificar", + "messaging.allowedSenders": "Remitentes permitidos", + "messaging.allowedSendersHint": "IDs de usuario separados por comas. Dejar vacío para permitir todos.", + "messaging.save": "Guardar →", + "messaging.add": "Añadir →", + "messaging.failedToSave": "Error al guardar", + "messaging.hint.telegram": "Crear un bot con @BotFather", + "messaging.hint.discord": "Crear un bot en el Portal de Desarrolladores", + "messaging.hint.slack": "Bot Token | App Token (separados por pipe)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (separados por pipe)", + "messaging.hint.signal": "URL de signal-cli | Teléfono (separados por pipe)", + "admin.gateway.keys": "Claves", + "admin.gateway.copy": "Copiar", + "admin.gateway.baseUrl": "URL base:", + "admin.gateway.apiKey": "Clave API:", + "admin.gateway.headers": "Encabezados:", + "admin.messaging": "Mensajería", + "admin.gateway.tab": "Gateway", + "keyboard.action.newFile": "Nuevo archivo", + "keyboard.action.newTerminal": "Nueva terminal", + "keyboard.action.newChat": "Nuevo chat", + "keyboard.action.closeTab": "Cerrar pestaña", + "keyboard.action.nextTab": "Siguiente pestaña", + "keyboard.action.prevTab": "Pestaña anterior", + "keyboard.action.quickOpen": "Apertura rápida", + "keyboard.action.searchAll": "Buscar", + "keyboard.action.openSettings": "Abrir ajustes", + "keyboard.action.toggleSplit": "Alternar división", + "keyboard.action.toggleSidebar": "Alternar barra lateral", + "keyboard.action.voiceMemo": "Nota de voz", + "keyboard.conflict": "También asignado a {{action}}", + "chat.history.justNow": "Ahora mismo", + "chat.history.minutesAgo": "hace {{count}}min", + "chat.history.hoursAgo": "hace {{count}}h", + "chat.history.daysAgo": "hace {{count}}d", + "chat.history.title": "Título", + "chat.history.updated": "Actualizado", + "chat.history.delete": "Eliminar", + "chat.edit.text": "Texto", + "chat.edit.thought": "Pensamiento", + "chat.edit.tool": "Herramienta", + "chat.edit.item": "Elemento", + "chat.dictate.unsupported": "El reconocimiento de voz no es compatible con este navegador.", + "chat.fallbackTitle": "Chat", + "admin.gateway.copied": "Copiado al portapapeles", + "admin.gateway.createError": "Error al crear la clave", + "admin.gateway.createKey": "Crear clave", + "admin.gateway.deleteError": "Error al eliminar la clave", + "admin.gateway.description": "Genera claves API para conectar Open WebUI o cualquier cliente compatible con OpenAI a tus espacios de trabajo.", + "admin.gateway.howToConnect": "Conectar desde Open WebUI", + "admin.gateway.keyCreated": "Clave API creada", + "admin.gateway.keyDeleted": "Clave API eliminada", + "admin.gateway.keyNamePlaceholder": "Nombre de la clave (ej. open-webui)", + "admin.gateway.keyWarning": "Esta clave solo se mostrará una vez. Guárdala de forma segura.", + "admin.gateway.loadError": "Error al cargar las claves API", + "admin.gateway.model": "Modelo de respuesta", + "admin.gateway.modelDescription": "Los clientes gateway eligen un espacio de trabajo; cptr usa este modelo para generar la respuesta.", + "admin.gateway.modelFallback": "Usar modelo del espacio de trabajo/predeterminado", + "admin.gateway.modelSaveError": "Error al guardar el modelo gateway", + "admin.gateway.newKey": "Nueva clave API creada. Cópiala ahora", + "admin.gateway.noKeys": "Aún no hay claves API", + "admin.gateway.title": "API Gateway", + "chat.greeting": "¿En qué puedo ayudarte?", + + "admin.subagents": "Sub-agentes", + "admin.subagentsEnabled": "Habilitar sub-agentes", + "admin.subagentsHint": "Permite a la IA delegar tareas a sub-agentes. Cada sub-agente crea un chat real con acceso completo a herramientas. Usa llamadas LLM adicionales.", + "admin.subagentsMaxConcurrent": "Máx. simultáneos", + "admin.subagentsMaxConcurrentHint": "sub-agentes simultáneos", + "admin.subagentsMaxIterations": "Máx. iteraciones", + "admin.subagentsMaxIterationsHint": "bucles de herramientas por sub-agente", + "admin.subagentsMaxOutput": "Máx. salida", + "admin.subagentsSystemPrompt": "Prompt del sistema", + "admin.subagentsSystemPromptPlaceholder": "Eres un sub-agente...", + "admin.subagentsSystemPromptHint": "Dejar vacío para el valor predeterminado." } diff --git a/cptr/frontend/src/lib/i18n/locales/fr.json b/cptr/frontend/src/lib/i18n/locales/fr.json index 5b6498b..a9fb176 100644 --- a/cptr/frontend/src/lib/i18n/locales/fr.json +++ b/cptr/frontend/src/lib/i18n/locales/fr.json @@ -295,7 +295,6 @@ "general.queueDesc": "Les messages envoyés pendant la génération sont mis en file et envoyés après la fin.", "general.interruptDesc": "L'envoi d'un message annule la génération en cours.", "settings.keyboard": "Clavier", - "settings.configuration": "Configuration", "keyboard.title": "Raccourcis clavier", "keyboard.resetDefaults": "Réinitialiser", "keyboard.command": "Commande", @@ -317,62 +316,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. Tous droits réservés.", "about.updateAvailable": "v{{version}} disponible", "connections.failedToUpdate": "Échec de la mise à jour de la connexion", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", + "keyboard.newFile": "Ouvrir un nouveau fichier vide", + "keyboard.newTerminal": "Ouvrir une nouvelle session de terminal", + "keyboard.newChat": "Démarrer un nouveau chat IA", + "keyboard.closeTab": "Fermer l'onglet actif", + "keyboard.nextTab": "Passer à l'onglet suivant", + "keyboard.prevTab": "Passer à l'onglet précédent", + "keyboard.quickOpen": "Rechercher et ouvrir des fichiers rapidement", + "keyboard.openSettings": "Ouvrir le panneau des paramètres", + "keyboard.toggleSplit": "Basculer la vue divisée de l'éditeur", + "keyboard.toggleSidebar": "Afficher ou masquer la barre latérale", + "automations.title": "Automatisations", + "automations.filter": "Filtrer...", + "automations.noMatches": "Aucun résultat", + "automations.noAutomations": "Aucune automatisation", + "automations.create": "Créer une automatisation", + "automations.status": "Statut", "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "automations.paused": "En pause", + "automations.all": "Toutes", + "automations.schedule": "Planification", + "automations.model": "Modèle", + "automations.nextRun": "Prochaine exécution", + "automations.lastRun": "Dernière exécution", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", + "automations.enabled": "Activé", + "automations.regenerate": "Régénérer", + "automations.disable": "Désactiver", + "automations.enable": "Activer", + "automations.copied": "Copié !", + "automations.copy": "Copier", "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", + "automations.runs": "Exécutions", + "automations.noRuns": "Aucune exécution", + "automations.viewChat": "voir le chat →", + "automations.deleted": "Supprimée", + "automations.webhookEnabled": "Webhook activé", + "automations.webhookDisabled": "Webhook désactivé", + "automations.failedToLoad": "Échec du chargement des automatisations", + "automations.failedToLoadOne": "Échec du chargement de l'automatisation", + "automations.failedToToggle": "Échec du basculement", + "automations.failedToDelete": "Échec de la suppression", + "automations.failedToRun": "Échec de l'exécution", + "automations.failedToGenerateWebhook": "Échec de la génération du webhook", + "automations.failedToRevokeWebhook": "Échec de la révocation du webhook", + "automations.triggered": "\"{{name}}\" déclenchée", + "automations.deleteConfirm": "Supprimer \"{{name}}\" ?", + "automations.runNow": "Exécuter maintenant", + "automations.newAutomation": "Nouvelle automatisation", + "automations.toggleSidebar": "Afficher/masquer la barre latérale", + "automationModal.titlePlaceholder": "Titre de l'automatisation", "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automationModal.promptPlaceholder": "Saisissez le prompt ici...", + "automationModal.selectWorkspace": "Sélectionner un espace de travail", + "automationModal.cancel": "Annuler", + "automationModal.saving": "Enregistrement...", + "automationModal.save": "Enregistrer", + "automationModal.createBtn": "Créer", + "automationModal.failedToSave": "Échec de la sauvegarde de l'automatisation", + "common.cancel": "Annuler", + "common.save": "Enregistrer", + "common.edit": "Modifier", + "common.copy": "Copier", + "common.remove": "Supprimer", + "common.close": "Fermer", + "common.dismiss": "Ignorer", + "common.loading": "Chargement", + "common.downloadCsv": "Télécharger CSV", + "chat.tool.readFile": "Lire {{path}}", + "chat.tool.readFileRange": "Lire {{path}} L{{range}}", + "chat.tool.editFile": "Modifier {{path}}", + "chat.tool.multiEditFile": "Édition multiple {{path}}", + "chat.tool.createFile": "Créer {{path}}", + "chat.tool.writeFile": "Écrire {{path}}", + "chat.tool.listDirectory": "Lister {{path}}", + "chat.tool.listDirectoryRecursive": "Lister {{path}} (récursif)", + "chat.tool.searchFiles": "Rechercher \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " dans {{include}}", + "chat.tool.backgroundCommand": "Arrière-plan : {{command}}", + "chat.tool.checkTask": "Vérifier la tâche {{id}}", + "chat.tool.killTask": "Terminer la tâche {{id}}", + "chat.tool.webSearch": "Recherche web : \"{{query}}\"", + "chat.tool.fetchUrl": "Récupérer {{hostname}}", + "chat.tool.fetchUrlFallback": "Récupérer l'URL", + "chat.saveAs": "Enregistrer sous", + "chat.send": "Envoyer", + "chat.artifact": "Artefact", + "chat.exploring": "Exploration", + "chat.explored": "Exploré", + "chat.allow": "Autoriser", + "chat.deny": "Refuser", + "chat.toolInput": "Entrée", + "chat.toolOutput": "Sortie", + "chat.toolLines": "Lignes {{start}}–{{end}}", + "chat.totalChars": "{{count}} caractères au total", + "chat.placeholder": "Posez une question sur {{name}}...", + "chat.prevResponse": "Réponse précédente", + "chat.nextResponse": "Réponse suivante", + "chat.editResponse": "Modifier la réponse", + "chat.copyResponse": "Copier la réponse", + "chat.regenerateResponse": "Régénérer la réponse", + "chat.usageInfo": "Informations d'utilisation", + "chat.toggleToolCalls": "Afficher/masquer les appels d'outils", + "chat.scrollToBottom": "Défiler vers le bas", + "chat.removeUpload": "Supprimer le fichier", + "chat.sendNow": "Envoyer maintenant", + "chat.prevMessage": "Message précédent", + "chat.nextMessage": "Message suivant", + "chat.editMessage": "Modifier le message", + "chat.copyMessage": "Copier le message", + "chat.editMessagePlaceholder": "Texte du message...", + "chat.editReasoningPlaceholder": "Texte du raisonnement...", + "plusMenu.askApproval": "Demander l'approbation", + "plusMenu.askApprovalDesc": "Confirmer chaque appel d'outil avant son exécution", + "plusMenu.autoApprove": "Approbation automatique", + "plusMenu.autoApproveDesc": "Approuver les actions sûres, demander pour les risquées", + "plusMenu.fullAccess": "Accès complet", + "plusMenu.fullAccessDesc": "Tous les appels s'exécutent sans confirmation", + "plusMenu.attachFiles": "Joindre des fichiers", + "plusMenu.capture": "Capturer", + "plusMenu.planMode": "Mode plan", + "plusMenu.toolPermissions": "Permissions des outils", + "plusMenu.parameters": "Paramètres", + "plusMenu.noParams": "Aucun paramètre configuré", + "plusMenu.paramKey": "clé", + "plusMenu.paramValue": "valeur", + "plusMenu.removeParam": "Supprimer le paramètre", + "plusMenu.addParam": "Ajouter un paramètre", + "voiceMemo.microphoneError": "Impossible d'accéder au microphone", + "voiceMemo.uploadFailed": "Audio sauvegardé localement. Échec de l'envoi au serveur.", + "voiceMemo.sttNotConfigured": "STT non configuré. Configurez dans Paramètres → Audio.", + "voiceMemo.writeFailed": "Échec de l'écriture du transcript. L'audio est en sécurité.", + "voiceMemo.done": "Terminé →", + "voiceMemo.processing": "Traitement…", + "voiceMemo.filename": "Nom du fichier", + "voiceMemo.filenamePlaceholder": "nom-enregistrement", + "voiceMemo.saved": "Enregistré ✓", + "admin.audio.title": "Audio", + "admin.audio.voiceMemos": "Notes vocales", + "admin.audio.enableVoiceMemos": "Activer les notes vocales", + "admin.audio.voiceMemosHint": "Enregistrer des notes vocales depuis le menu \"+\".", + "admin.audio.autoTranscribe": "Transcription automatique", + "admin.audio.transcribeOnHint": "Les enregistrements sont transcrits en markdown via STT.", + "admin.audio.transcribeOffHint": "Les enregistrements sont sauvegardés en audio uniquement.", + "admin.audio.recordingQuality": "Qualité d'enregistrement", + "admin.audio.qualityHigh": "Haute (128kbps)", + "admin.audio.qualityMedium": "Moyenne (64kbps)", + "admin.audio.qualityLow": "Basse (32kbps)", + "admin.audio.qualityHintHigh": "Meilleure qualité, fichiers plus volumineux.", + "admin.audio.qualityHintMedium": "Qualité et taille équilibrées.", + "admin.audio.qualityHintLow": "Fichiers les plus petits, optimisé pour la parole.", + "admin.audio.stt": "Reconnaissance vocale", + "admin.audio.sttHint": "Compatible avec l'API audio/transcriptions d'OpenAI.", + "admin.audio.saveFailed": "Échec de la sauvegarde des paramètres audio", + "sqlite.loadError": "Impossible de charger la base de données.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}o]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Exécuter ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} sur {{total}}", + "sqlite.prev": "← Précédent", + "sqlite.next": "Suivant →", + "preview.htmlTitle": "Aperçu HTML", + "preview.emptySheet": "Feuille vide", + "port.cannotConnect": "Connexion impossible", + "connections.optional": "Facultatif", + "connections.leaveBlankPlaceholder": "•••••••• (laisser vide pour conserver)", + "a11y.dismissNotification": "Ignorer la notification", + "a11y.chatOptions": "Options du chat", + "a11y.prevPage": "Page précédente", + "a11y.nextPage": "Page suivante", + "a11y.splitEditor": "Diviser l'éditeur", + "a11y.closePane": "Fermer le panneau", + "a11y.refreshChanges": "Actualiser les modifications", + "keyboard.clickToRebind": "Cliquer pour réassigner", + "bar.splitRight": "Diviser à droite", + "bar.splitDown": "Diviser en bas", + "bar.voiceMemo": "Note vocale", + "bar.files": "Fichiers", + "messaging.noBots": "Aucun bot de messagerie configuré.", + "messaging.failedToLoad": "Échec du chargement des bots", + "messaging.failedToToggle": "Échec du basculement du bot", + "messaging.failedToDelete": "Échec de la suppression du bot", + "messaging.editBot": "Modifier le bot", + "messaging.addBot": "Ajouter un bot", + "messaging.name": "Nom", + "messaging.platform": "Plateforme", + "messaging.token": "Jeton", + "messaging.tokenKeep": "Laisser vide pour conserver", + "messaging.tokenPaste": "Coller le jeton", + "messaging.verify": "Vérifier", + "messaging.allowedSenders": "Expéditeurs autorisés", + "messaging.allowedSendersHint": "IDs d'utilisateurs séparés par des virgules. Laisser vide pour autoriser tous.", + "messaging.save": "Enregistrer →", + "messaging.add": "Ajouter →", + "messaging.failedToSave": "Échec de la sauvegarde", + "messaging.hint.telegram": "Créer un bot via @BotFather", + "messaging.hint.discord": "Créer un bot dans le Portail Développeur", + "messaging.hint.slack": "Bot Token | App Token (séparés par pipe)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (séparés par pipe)", + "messaging.hint.signal": "URL signal-cli | Numéro de téléphone (séparés par pipe)", + "admin.gateway.keys": "Clés", + "admin.gateway.copy": "Copier", + "admin.gateway.baseUrl": "URL de base :", + "admin.gateway.apiKey": "Clé API :", + "admin.gateway.headers": "En-têtes :", + "admin.messaging": "Messagerie", + "admin.gateway.tab": "Passerelle", + "keyboard.action.newFile": "Nouveau fichier", + "keyboard.action.newTerminal": "Nouveau terminal", + "keyboard.action.newChat": "Nouveau chat", + "keyboard.action.closeTab": "Fermer l'onglet", + "keyboard.action.nextTab": "Onglet suivant", + "keyboard.action.prevTab": "Onglet précédent", + "keyboard.action.quickOpen": "Ouverture rapide", + "keyboard.action.searchAll": "Rechercher", + "keyboard.action.openSettings": "Ouvrir les paramètres", + "keyboard.action.toggleSplit": "Basculer la division", + "keyboard.action.toggleSidebar": "Basculer la barre latérale", + "keyboard.action.voiceMemo": "Note vocale", + "keyboard.conflict": "Également lié à {{action}}", + "chat.history.justNow": "À l'instant", + "chat.history.minutesAgo": "il y a {{count}}min", + "chat.history.hoursAgo": "il y a {{count}}h", + "chat.history.daysAgo": "il y a {{count}}j", + "chat.history.title": "Titre", + "chat.history.updated": "Mis à jour", + "chat.history.delete": "Supprimer", + "chat.edit.text": "Texte", + "chat.edit.thought": "Réflexion", + "chat.edit.tool": "Outil", + "chat.edit.item": "Élément", + "chat.dictate.unsupported": "La reconnaissance vocale n'est pas prise en charge dans ce navigateur.", + "chat.fallbackTitle": "Chat", + "admin.gateway.copied": "Copié dans le presse-papiers", + "admin.gateway.createError": "Échec de la création de la clé", + "admin.gateway.createKey": "Créer une clé", + "admin.gateway.deleteError": "Échec de la suppression de la clé", + "admin.gateway.description": "Générez des clés API pour connecter Open WebUI ou tout client compatible OpenAI à vos espaces de travail.", + "admin.gateway.howToConnect": "Se connecter depuis Open WebUI", + "admin.gateway.keyCreated": "Clé API créée", + "admin.gateway.keyDeleted": "Clé API supprimée", + "admin.gateway.keyNamePlaceholder": "Nom de la clé (ex. open-webui)", + "admin.gateway.keyWarning": "Cette clé ne sera affichée qu'une seule fois. Conservez-la en sécurité.", + "admin.gateway.loadError": "Échec du chargement des clés API", + "admin.gateway.model": "Modèle de réponse", + "admin.gateway.modelDescription": "Les clients passerelle choisissent un espace de travail ; cptr utilise ce modèle pour générer la réponse.", + "admin.gateway.modelFallback": "Utiliser le modèle de l'espace de travail/par défaut", + "admin.gateway.modelSaveError": "Échec de l'enregistrement du modèle passerelle", + "admin.gateway.newKey": "Nouvelle clé API créée. Copiez-la maintenant", + "admin.gateway.noKeys": "Aucune clé API", + "admin.gateway.title": "Passerelle API", + "chat.greeting": "Comment puis-je vous aider ?", + + "admin.subagents": "Sous-agents", + "admin.subagentsEnabled": "Activer les sous-agents", + "admin.subagentsHint": "Permet à l'IA de déléguer des tâches à des sous-agents. Chaque sous-agent crée un vrai chat avec un accès complet aux outils. Utilise des appels LLM supplémentaires.", + "admin.subagentsMaxConcurrent": "Max. simultanés", + "admin.subagentsMaxConcurrentHint": "sous-agents simultanés", + "admin.subagentsMaxIterations": "Max. itérations", + "admin.subagentsMaxIterationsHint": "boucles d'outils par sous-agent", + "admin.subagentsMaxOutput": "Max. sortie", + "admin.subagentsSystemPrompt": "Prompt système", + "admin.subagentsSystemPromptPlaceholder": "Vous êtes un sous-agent...", + "admin.subagentsSystemPromptHint": "Laisser vide pour la valeur par défaut." } diff --git a/cptr/frontend/src/lib/i18n/locales/ja.json b/cptr/frontend/src/lib/i18n/locales/ja.json index 87b1d3c..9db4828 100644 --- a/cptr/frontend/src/lib/i18n/locales/ja.json +++ b/cptr/frontend/src/lib/i18n/locales/ja.json @@ -235,7 +235,7 @@ "system.disk": "ディスク", "system.cores": "{{count}} コア", "system.process": "プロセス", - "admin.web": "Web", + "admin.web": "ウェブ", "admin.webEnabled": "Webアクセスを有効にする", "admin.webEnabledHint": "AIがWebを検索し、URLを取得できます。", "admin.webDisabledHint": "Web検索とURL取得は無効です。", @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. All rights reserved.", "about.updateAvailable": "v{{version}} が利用可能", "connections.failedToUpdate": "接続の更新に失敗しました", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "keyboard.newFile": "新しい空のファイルを開く", + "keyboard.newTerminal": "新しいターミナルセッションを開く", + "keyboard.newChat": "新しいAIチャットを開始", + "keyboard.closeTab": "アクティブなタブを閉じる", + "keyboard.nextTab": "次のタブに切り替える", + "keyboard.prevTab": "前のタブに切り替える", + "keyboard.quickOpen": "ファイルをすばやく検索して開く", + "keyboard.openSettings": "設定パネルを開く", + "keyboard.toggleSplit": "エディタの分割表示を切替", + "keyboard.toggleSidebar": "サイドバーの表示/非表示", + "automations.title": "自動化", + "automations.filter": "フィルター...", + "automations.noMatches": "一致なし", + "automations.noAutomations": "自動化はまだありません", + "automations.create": "自動化を作成", + "automations.status": "ステータス", + "automations.active": "有効", + "automations.paused": "一時停止", + "automations.all": "すべて", + "automations.schedule": "スケジュール", + "automations.model": "モデル", + "automations.nextRun": "次の実行", + "automations.lastRun": "前回の実行", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", - "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.enabled": "有効", + "automations.regenerate": "再生成", + "automations.disable": "無効にする", + "automations.enable": "有効にする", + "automations.copied": "コピーしました!", + "automations.copy": "コピー", + "automations.prompt": "プロンプト", + "automations.runs": "実行履歴", + "automations.noRuns": "まだ実行されていません", + "automations.viewChat": "チャットを見る →", + "automations.deleted": "削除済み", + "automations.webhookEnabled": "Webhook有効", + "automations.webhookDisabled": "Webhook無効", + "automations.failedToLoad": "自動化の読み込みに失敗しました", + "automations.failedToLoadOne": "自動化の読み込みに失敗しました", + "automations.failedToToggle": "切り替えに失敗しました", + "automations.failedToDelete": "削除に失敗しました", + "automations.failedToRun": "実行に失敗しました", + "automations.failedToGenerateWebhook": "Webhookの生成に失敗しました", + "automations.failedToRevokeWebhook": "Webhookの取り消しに失敗しました", + "automations.triggered": "\"{{name}}\" が実行されました", + "automations.deleteConfirm": "\"{{name}}\" を削除しますか?", + "automations.runNow": "今すぐ実行", + "automations.newAutomation": "新しい自動化", + "automations.toggleSidebar": "サイドバーを切り替え", + "automationModal.titlePlaceholder": "自動化のタイトル", + "automationModal.instructions": "手順", + "automationModal.promptPlaceholder": "プロンプトを入力...", + "automationModal.selectWorkspace": "ワークスペースを選択", + "automationModal.cancel": "キャンセル", + "automationModal.saving": "保存中...", + "automationModal.save": "保存", + "automationModal.createBtn": "作成", + "automationModal.failedToSave": "自動化の保存に失敗しました", + "common.cancel": "キャンセル", + "common.save": "保存", + "common.edit": "編集", + "common.copy": "コピー", + "common.remove": "削除", + "common.close": "閉じる", + "common.dismiss": "閉じる", + "common.loading": "読み込み中", + "common.downloadCsv": "CSVをダウンロード", + "chat.tool.readFile": "読み取り {{path}}", + "chat.tool.readFileRange": "読み取り {{path}} L{{range}}", + "chat.tool.editFile": "編集 {{path}}", + "chat.tool.multiEditFile": "複数編集 {{path}}", + "chat.tool.createFile": "作成 {{path}}", + "chat.tool.writeFile": "書き込み {{path}}", + "chat.tool.listDirectory": "一覧 {{path}}", + "chat.tool.listDirectoryRecursive": "一覧 {{path}} (再帰)", + "chat.tool.searchFiles": "検索 \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " ({{include}}内)", + "chat.tool.backgroundCommand": "バックグラウンド: {{command}}", + "chat.tool.checkTask": "タスク確認 {{id}}", + "chat.tool.killTask": "タスク終了 {{id}}", + "chat.tool.webSearch": "Web検索: \"{{query}}\"", + "chat.tool.fetchUrl": "取得 {{hostname}}", + "chat.tool.fetchUrlFallback": "URL取得", + "chat.saveAs": "名前を付けて保存", + "chat.send": "送信", + "chat.artifact": "アーティファクト", + "chat.exploring": "探索中", + "chat.explored": "探索済み", + "chat.allow": "許可", + "chat.deny": "拒否", + "chat.toolInput": "入力", + "chat.toolOutput": "出力", + "chat.toolLines": "行 {{start}}–{{end}}", + "chat.totalChars": "合計 {{count}} 文字", + "chat.placeholder": "{{name}}について何でも聞いてください...", + "chat.prevResponse": "前の応答", + "chat.nextResponse": "次の応答", + "chat.editResponse": "応答を編集", + "chat.copyResponse": "応答をコピー", + "chat.regenerateResponse": "応答を再生成", + "chat.usageInfo": "使用状況", + "chat.toggleToolCalls": "ツール呼び出しの表示切替", + "chat.scrollToBottom": "一番下にスクロール", + "chat.removeUpload": "アップロードを削除", + "chat.sendNow": "今すぐ送信", + "chat.prevMessage": "前のメッセージ", + "chat.nextMessage": "次のメッセージ", + "chat.editMessage": "メッセージを編集", + "chat.copyMessage": "メッセージをコピー", + "chat.editMessagePlaceholder": "メッセージテキスト...", + "chat.editReasoningPlaceholder": "推論テキスト...", + "plusMenu.askApproval": "承認を求める", + "plusMenu.askApprovalDesc": "各ツール呼び出しの実行前に確認", + "plusMenu.autoApprove": "自動承認", + "plusMenu.autoApproveDesc": "安全な操作は承認、リスクのあるものは確認", + "plusMenu.fullAccess": "フルアクセス", + "plusMenu.fullAccessDesc": "すべてのツール呼び出しを確認なしで実行", + "plusMenu.attachFiles": "ファイルを添付", + "plusMenu.capture": "キャプチャ", + "plusMenu.planMode": "プランモード", + "plusMenu.toolPermissions": "ツール権限", + "plusMenu.parameters": "パラメータ", + "plusMenu.noParams": "パラメータは設定されていません", + "plusMenu.paramKey": "キー", + "plusMenu.paramValue": "値", + "plusMenu.removeParam": "パラメータを削除", + "plusMenu.addParam": "パラメータを追加", + "voiceMemo.microphoneError": "マイクにアクセスできません", + "voiceMemo.uploadFailed": "音声はローカルに保存されました。サーバーへのアップロードに失敗しました。", + "voiceMemo.sttNotConfigured": "STTが未設定です。設定 → オーディオで設定してください。", + "voiceMemo.writeFailed": "文字起こしの書き込みに失敗しました。音声は安全です。", + "voiceMemo.done": "完了 →", + "voiceMemo.processing": "処理中…", + "voiceMemo.filename": "ファイル名", + "voiceMemo.filenamePlaceholder": "録音名", + "voiceMemo.saved": "保存済み ✓", + "admin.audio.title": "オーディオ", + "admin.audio.voiceMemos": "音声メモ", + "admin.audio.enableVoiceMemos": "音声メモを有効にする", + "admin.audio.voiceMemosHint": "\"+\"メニューから音声メモを録音します。", + "admin.audio.autoTranscribe": "自動文字起こし", + "admin.audio.transcribeOnHint": "録音はSTTでMarkdownに文字起こしされます。", + "admin.audio.transcribeOffHint": "録音は音声のみで保存されます。", + "admin.audio.recordingQuality": "録音品質", + "admin.audio.qualityHigh": "高品質 (128kbps)", + "admin.audio.qualityMedium": "中品質 (64kbps)", + "admin.audio.qualityLow": "低品質 (32kbps)", + "admin.audio.qualityHintHigh": "最高品質、ファイルサイズ大。", + "admin.audio.qualityHintMedium": "品質とサイズのバランス。", + "admin.audio.qualityHintLow": "最小ファイル、音声に最適化。", + "admin.audio.stt": "音声テキスト変換", + "admin.audio.sttHint": "OpenAIのaudio/transcriptions APIと互換性があります。", + "admin.audio.saveFailed": "オーディオ設定の保存に失敗しました", + "sqlite.loadError": "データベースの読み込みに失敗しました。", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "実行 ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} / {{total}}", + "sqlite.prev": "← 前へ", + "sqlite.next": "次へ →", + "preview.htmlTitle": "HTMLプレビュー", + "preview.emptySheet": "空のシート", + "port.cannotConnect": "接続できません", + "connections.optional": "任意", + "connections.leaveBlankPlaceholder": "•••••••• (空欄で現在の値を保持)", + "a11y.dismissNotification": "通知を閉じる", + "a11y.chatOptions": "チャットオプション", + "a11y.prevPage": "前のページ", + "a11y.nextPage": "次のページ", + "a11y.splitEditor": "エディタを分割", + "a11y.closePane": "パネルを閉じる", + "a11y.refreshChanges": "変更を更新", + "keyboard.clickToRebind": "クリックして再割り当て", + "bar.splitRight": "右に分割", + "bar.splitDown": "下に分割", + "bar.voiceMemo": "音声メモ", + "bar.files": "ファイル", + "messaging.noBots": "メッセージングボットが設定されていません。", + "messaging.failedToLoad": "ボットの読み込みに失敗しました", + "messaging.failedToToggle": "ボットの切り替えに失敗しました", + "messaging.failedToDelete": "ボットの削除に失敗しました", + "messaging.editBot": "ボットを編集", + "messaging.addBot": "ボットを追加", + "messaging.name": "名前", + "messaging.platform": "プラットフォーム", + "messaging.token": "トークン", + "messaging.tokenKeep": "現在の値を保持するには空欄", + "messaging.tokenPaste": "トークンを貼り付け", + "messaging.verify": "確認", + "messaging.allowedSenders": "許可された送信者", + "messaging.allowedSendersHint": "カンマ区切りのユーザーID。空欄で全員を許可。", + "messaging.save": "保存 →", + "messaging.add": "追加 →", + "messaging.failedToSave": "保存に失敗しました", + "messaging.hint.telegram": "@BotFatherでボットを作成", + "messaging.hint.discord": "Developer Portalでボットを作成", + "messaging.hint.slack": "Bot Token | App Token (パイプ区切り)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (パイプ区切り)", + "messaging.hint.signal": "signal-cli URL | 電話番号 (パイプ区切り)", + "admin.gateway.keys": "キー", + "admin.gateway.copy": "コピー", + "admin.gateway.baseUrl": "ベースURL:", + "admin.gateway.apiKey": "APIキー:", + "admin.gateway.headers": "ヘッダー:", + "admin.messaging": "メッセージング", + "admin.gateway.tab": "ゲートウェイ", + "keyboard.action.newFile": "新規ファイル", + "keyboard.action.newTerminal": "新規ターミナル", + "keyboard.action.newChat": "新規チャット", + "keyboard.action.closeTab": "タブを閉じる", + "keyboard.action.nextTab": "次のタブ", + "keyboard.action.prevTab": "前のタブ", + "keyboard.action.quickOpen": "クイックオープン", + "keyboard.action.searchAll": "検索", + "keyboard.action.openSettings": "設定を開く", + "keyboard.action.toggleSplit": "分割を切替", + "keyboard.action.toggleSidebar": "サイドバーを切替", + "keyboard.action.voiceMemo": "音声メモ", + "keyboard.conflict": "{{action}}にも割り当て済み", + "chat.history.justNow": "たった今", + "chat.history.minutesAgo": "{{count}}分前", + "chat.history.hoursAgo": "{{count}}時間前", + "chat.history.daysAgo": "{{count}}日前", + "chat.history.title": "タイトル", + "chat.history.updated": "更新日", + "chat.history.delete": "削除", + "chat.edit.text": "テキスト", + "chat.edit.thought": "思考", + "chat.edit.tool": "ツール", + "chat.edit.item": "項目", + "chat.dictate.unsupported": "このブラウザでは音声認識がサポートされていません。", + "chat.fallbackTitle": "チャット", + "admin.gateway.copied": "クリップボードにコピーしました", + "admin.gateway.createError": "キーの作成に失敗しました", + "admin.gateway.createKey": "キーを作成", + "admin.gateway.deleteError": "キーの削除に失敗しました", + "admin.gateway.description": "APIキーを生成して、Open WebUIまたはOpenAI互換クライアントをワークスペースに接続します。", + "admin.gateway.howToConnect": "Open WebUIから接続", + "admin.gateway.keyCreated": "APIキーが作成されました", + "admin.gateway.keyDeleted": "APIキーが削除されました", + "admin.gateway.keyNamePlaceholder": "キー名(例:open-webui)", + "admin.gateway.keyWarning": "このキーは一度だけ表示されます。安全に保管してください。", + "admin.gateway.loadError": "APIキーの読み込みに失敗しました", + "admin.gateway.model": "応答モデル", + "admin.gateway.modelDescription": "ゲートウェイクライアントがワークスペースを選択し、cptrがこのモデルで応答を生成します。", + "admin.gateway.modelFallback": "ワークスペース/デフォルトモデルを使用", + "admin.gateway.modelSaveError": "ゲートウェイモデルの保存に失敗しました", + "admin.gateway.newKey": "新しいAPIキーが作成されました。今すぐコピーしてください", + "admin.gateway.noKeys": "APIキーがまだありません", + "admin.gateway.title": "APIゲートウェイ", + "chat.greeting": "何かお手伝いできますか?", + + "admin.subagents": "サブエージェント", + "admin.subagentsEnabled": "サブエージェントを有効にする", + "admin.subagentsHint": "AIがサブエージェントにタスクを委任できるようにします。各サブエージェントはフルツールアクセス付きの実際のチャットを作成します。追加のLLMコールを使用します。", + "admin.subagentsMaxConcurrent": "最大同時実行数", + "admin.subagentsMaxConcurrentHint": "同時実行サブエージェント", + "admin.subagentsMaxIterations": "最大反復回数", + "admin.subagentsMaxIterationsHint": "サブエージェントあたりのツールループ", + "admin.subagentsMaxOutput": "最大出力", + "admin.subagentsSystemPrompt": "システムプロンプト", + "admin.subagentsSystemPromptPlaceholder": "あなたはサブエージェントです...", + "admin.subagentsSystemPromptHint": "組み込みデフォルトを使用する場合は空のままにしてください。" } diff --git a/cptr/frontend/src/lib/i18n/locales/ko.json b/cptr/frontend/src/lib/i18n/locales/ko.json index bcf9602..4055b37 100644 --- a/cptr/frontend/src/lib/i18n/locales/ko.json +++ b/cptr/frontend/src/lib/i18n/locales/ko.json @@ -174,7 +174,7 @@ "admin.displayName": "표시 이름", "admin.passwordLabel": "비밀번호", "admin.role": "역할", - "admin.optional": "선택 사항", + "admin.optional": "선택사항", "admin.minChars": "최소 6자", "admin.enterUsername": "사용자 이름 입력", "admin.create": "생성", @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. All rights reserved.", "about.updateAvailable": "v{{version}} 사용 가능", "connections.failedToUpdate": "연결 업데이트에 실패했습니다", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", - "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", - "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "keyboard.newFile": "새 빈 파일 열기", + "keyboard.newTerminal": "새 터미널 세션 열기", + "keyboard.newChat": "새 AI 채팅 시작", + "keyboard.closeTab": "활성 탭 닫기", + "keyboard.nextTab": "다음 탭으로 전환", + "keyboard.prevTab": "이전 탭으로 전환", + "keyboard.quickOpen": "파일을 빠르게 검색하고 열기", + "keyboard.openSettings": "설정 패널 열기", + "keyboard.toggleSplit": "편집기 분할 보기 전환", + "keyboard.toggleSidebar": "사이드바 표시/숨기기", + "automations.title": "자동화", + "automations.filter": "필터...", + "automations.noMatches": "일치 항목 없음", + "automations.noAutomations": "자동화가 아직 없습니다", + "automations.create": "자동화 만들기", + "automations.status": "상태", + "automations.active": "활성", + "automations.paused": "일시중지", + "automations.all": "전체", + "automations.schedule": "일정", + "automations.model": "모델", + "automations.nextRun": "다음 실행", + "automations.lastRun": "마지막 실행", + "automations.webhook": "웹훅", + "automations.enabled": "활성화됨", + "automations.regenerate": "재생성", + "automations.disable": "비활성화", + "automations.enable": "활성화", + "automations.copied": "복사됨!", + "automations.copy": "복사", + "automations.prompt": "프롬프트", + "automations.runs": "실행 기록", + "automations.noRuns": "아직 실행 기록 없음", + "automations.viewChat": "채팅 보기 →", + "automations.deleted": "삭제됨", + "automations.webhookEnabled": "웹훅 활성화됨", + "automations.webhookDisabled": "웹훅 비활성화됨", + "automations.failedToLoad": "자동화 로드에 실패했습니다", + "automations.failedToLoadOne": "자동화 로드에 실패했습니다", + "automations.failedToToggle": "전환에 실패했습니다", + "automations.failedToDelete": "삭제에 실패했습니다", + "automations.failedToRun": "실행에 실패했습니다", + "automations.failedToGenerateWebhook": "웹훅 생성에 실패했습니다", + "automations.failedToRevokeWebhook": "웹훅 해지에 실패했습니다", + "automations.triggered": "\"{{name}}\" 실행됨", + "automations.deleteConfirm": "\"{{name}}\"을(를) 삭제하시겠습니까?", + "automations.runNow": "지금 실행", + "automations.newAutomation": "새 자동화", + "automations.toggleSidebar": "사이드바 전환", + "automationModal.titlePlaceholder": "자동화 제목", + "automationModal.instructions": "지침", + "automationModal.promptPlaceholder": "프롬프트를 입력하세요...", + "automationModal.selectWorkspace": "작업 공간 선택", + "automationModal.cancel": "취소", + "automationModal.saving": "저장 중...", + "automationModal.save": "저장", + "automationModal.createBtn": "만들기", + "automationModal.failedToSave": "자동화 저장에 실패했습니다", + "common.cancel": "취소", + "common.save": "저장", + "common.edit": "편집", + "common.copy": "복사", + "common.remove": "삭제", + "common.close": "닫기", + "common.dismiss": "닫기", + "common.loading": "로딩 중", + "common.downloadCsv": "CSV 다운로드", + "chat.tool.readFile": "읽기 {{path}}", + "chat.tool.readFileRange": "읽기 {{path}} L{{range}}", + "chat.tool.editFile": "편집 {{path}}", + "chat.tool.multiEditFile": "다중 편집 {{path}}", + "chat.tool.createFile": "생성 {{path}}", + "chat.tool.writeFile": "쓰기 {{path}}", + "chat.tool.listDirectory": "목록 {{path}}", + "chat.tool.listDirectoryRecursive": "목록 {{path}} (재귀)", + "chat.tool.searchFiles": "검색 \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " ({{include}} 내)", + "chat.tool.backgroundCommand": "백그라운드: {{command}}", + "chat.tool.checkTask": "작업 확인 {{id}}", + "chat.tool.killTask": "작업 종료 {{id}}", + "chat.tool.webSearch": "웹 검색: \"{{query}}\"", + "chat.tool.fetchUrl": "가져오기 {{hostname}}", + "chat.tool.fetchUrlFallback": "URL 가져오기", + "chat.saveAs": "다른 이름으로 저장", + "chat.send": "전송", + "chat.artifact": "아티팩트", + "chat.exploring": "탐색 중", + "chat.explored": "탐색 완료", + "chat.allow": "허용", + "chat.deny": "거부", + "chat.toolInput": "입력", + "chat.toolOutput": "출력", + "chat.toolLines": "줄 {{start}}–{{end}}", + "chat.totalChars": "총 {{count}}자", + "chat.placeholder": "{{name}}에 대해 무엇이든 물어보세요...", + "chat.prevResponse": "이전 응답", + "chat.nextResponse": "다음 응답", + "chat.editResponse": "응답 편집", + "chat.copyResponse": "응답 복사", + "chat.regenerateResponse": "응답 재생성", + "chat.usageInfo": "사용량 정보", + "chat.toggleToolCalls": "도구 호출 표시 전환", + "chat.scrollToBottom": "맨 아래로 스크롤", + "chat.removeUpload": "업로드 삭제", + "chat.sendNow": "지금 전송", + "chat.prevMessage": "이전 메시지", + "chat.nextMessage": "다음 메시지", + "chat.editMessage": "메시지 편집", + "chat.copyMessage": "메시지 복사", + "chat.editMessagePlaceholder": "메시지 텍스트...", + "chat.editReasoningPlaceholder": "추론 텍스트...", + "plusMenu.askApproval": "승인 요청", + "plusMenu.askApprovalDesc": "각 도구 호출 실행 전 확인", + "plusMenu.autoApprove": "자동 승인", + "plusMenu.autoApproveDesc": "안전한 작업은 승인, 위험한 작업은 확인", + "plusMenu.fullAccess": "전체 접근", + "plusMenu.fullAccessDesc": "모든 도구 호출을 확인 없이 실행", + "plusMenu.attachFiles": "파일 첨부", + "plusMenu.capture": "캡처", + "plusMenu.planMode": "계획 모드", + "plusMenu.toolPermissions": "도구 권한", + "plusMenu.parameters": "매개변수", + "plusMenu.noParams": "설정된 매개변수 없음", + "plusMenu.paramKey": "키", + "plusMenu.paramValue": "값", + "plusMenu.removeParam": "매개변수 삭제", + "plusMenu.addParam": "매개변수 추가", + "voiceMemo.microphoneError": "마이크에 접근할 수 없습니다", + "voiceMemo.uploadFailed": "오디오가 로컬에 저장되었습니다. 서버 업로드에 실패했습니다.", + "voiceMemo.sttNotConfigured": "STT가 설정되지 않았습니다. 설정 → 오디오에서 설정하세요.", + "voiceMemo.writeFailed": "자막 작성에 실패했습니다. 오디오는 안전합니다.", + "voiceMemo.done": "완료 →", + "voiceMemo.processing": "처리 중…", + "voiceMemo.filename": "파일 이름", + "voiceMemo.filenamePlaceholder": "녹음-이름", + "voiceMemo.saved": "저장됨 ✓", + "admin.audio.title": "오디오", + "admin.audio.voiceMemos": "음성 메모", + "admin.audio.enableVoiceMemos": "음성 메모 활성화", + "admin.audio.voiceMemosHint": "\"+\" 메뉴에서 음성 메모를 녹음합니다.", + "admin.audio.autoTranscribe": "자동 전사", + "admin.audio.transcribeOnHint": "녹음이 STT를 통해 마크다운으로 전사됩니다.", + "admin.audio.transcribeOffHint": "녹음이 오디오로만 저장됩니다.", + "admin.audio.recordingQuality": "녹음 품질", + "admin.audio.qualityHigh": "고품질 (128kbps)", + "admin.audio.qualityMedium": "중간 (64kbps)", + "admin.audio.qualityLow": "저품질 (32kbps)", + "admin.audio.qualityHintHigh": "최고 품질, 파일 크기 큼.", + "admin.audio.qualityHintMedium": "품질과 크기의 균형.", + "admin.audio.qualityHintLow": "가장 작은 파일, 음성에 최적화.", + "admin.audio.stt": "음성-텍스트 변환", + "admin.audio.sttHint": "OpenAI의 audio/transcriptions API와 호환됩니다.", + "admin.audio.saveFailed": "오디오 설정 저장에 실패했습니다", + "sqlite.loadError": "데이터베이스를 불러오지 못했습니다.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "실행 ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} / {{total}}", + "sqlite.prev": "← 이전", + "sqlite.next": "다음 →", + "preview.htmlTitle": "HTML 미리보기", + "preview.emptySheet": "빈 시트", + "port.cannotConnect": "연결할 수 없습니다", + "connections.optional": "선택사항", + "connections.leaveBlankPlaceholder": "•••••••• (비워두면 현재 값 유지)", + "a11y.dismissNotification": "알림 닫기", + "a11y.chatOptions": "채팅 옵션", + "a11y.prevPage": "이전 페이지", + "a11y.nextPage": "다음 페이지", + "a11y.splitEditor": "편집기 분할", + "a11y.closePane": "패널 닫기", + "a11y.refreshChanges": "변경사항 새로고침", + "keyboard.clickToRebind": "클릭하여 재할당", + "bar.splitRight": "오른쪽으로 분할", + "bar.splitDown": "아래로 분할", + "bar.voiceMemo": "음성 메모", + "bar.files": "파일", + "messaging.noBots": "구성된 메시징 봇이 없습니다.", + "messaging.failedToLoad": "봇 로드에 실패했습니다", + "messaging.failedToToggle": "봇 전환에 실패했습니다", + "messaging.failedToDelete": "봇 삭제에 실패했습니다", + "messaging.editBot": "봇 편집", + "messaging.addBot": "봇 추가", + "messaging.name": "이름", + "messaging.platform": "플랫폼", + "messaging.token": "토큰", + "messaging.tokenKeep": "현재 값을 유지하려면 비워두세요", + "messaging.tokenPaste": "토큰 붙여넣기", + "messaging.verify": "확인", + "messaging.allowedSenders": "허용된 발신자", + "messaging.allowedSendersHint": "쉼표로 구분된 사용자 ID. 비워두면 전체 허용.", + "messaging.save": "저장 →", + "messaging.add": "추가 →", + "messaging.failedToSave": "저장에 실패했습니다", + "messaging.hint.telegram": "@BotFather에서 봇 생성", + "messaging.hint.discord": "개발자 포털에서 봇 생성", + "messaging.hint.slack": "Bot Token | App Token (파이프 구분)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (파이프 구분)", + "messaging.hint.signal": "signal-cli URL | 전화번호 (파이프 구분)", + "admin.gateway.keys": "키", + "admin.gateway.copy": "복사", + "admin.gateway.baseUrl": "기본 URL:", + "admin.gateway.apiKey": "API 키:", + "admin.gateway.headers": "헤더:", + "admin.messaging": "메시징", + "admin.gateway.tab": "게이트웨이", + "keyboard.action.newFile": "새 파일", + "keyboard.action.newTerminal": "새 터미널", + "keyboard.action.newChat": "새 채팅", + "keyboard.action.closeTab": "탭 닫기", + "keyboard.action.nextTab": "다음 탭", + "keyboard.action.prevTab": "이전 탭", + "keyboard.action.quickOpen": "빠른 열기", + "keyboard.action.searchAll": "검색", + "keyboard.action.openSettings": "설정 열기", + "keyboard.action.toggleSplit": "분할 전환", + "keyboard.action.toggleSidebar": "사이드바 전환", + "keyboard.action.voiceMemo": "음성 메모", + "keyboard.conflict": "{{action}}에도 할당됨", + "chat.history.justNow": "방금", + "chat.history.minutesAgo": "{{count}}분 전", + "chat.history.hoursAgo": "{{count}}시간 전", + "chat.history.daysAgo": "{{count}}일 전", + "chat.history.title": "제목", + "chat.history.updated": "수정됨", + "chat.history.delete": "삭제", + "chat.edit.text": "텍스트", + "chat.edit.thought": "생각", + "chat.edit.tool": "도구", + "chat.edit.item": "항목", + "chat.dictate.unsupported": "이 브라우저에서는 음성 인식이 지원되지 않습니다.", + "chat.fallbackTitle": "채팅", + "admin.gateway.copied": "클립보드에 복사됨", + "admin.gateway.createError": "키 생성에 실패했습니다", + "admin.gateway.createKey": "키 생성", + "admin.gateway.deleteError": "키 삭제에 실패했습니다", + "admin.gateway.description": "API 키를 생성하여 Open WebUI 또는 OpenAI 호환 클라이언트를 작업 공간에 연결합니다.", + "admin.gateway.howToConnect": "Open WebUI에서 연결", + "admin.gateway.keyCreated": "API 키가 생성되었습니다", + "admin.gateway.keyDeleted": "API 키가 삭제되었습니다", + "admin.gateway.keyNamePlaceholder": "키 이름 (예: open-webui)", + "admin.gateway.keyWarning": "이 키는 한 번만 표시됩니다. 안전하게 보관하세요.", + "admin.gateway.loadError": "API 키 로드에 실패했습니다", + "admin.gateway.model": "응답 모델", + "admin.gateway.modelDescription": "게이트웨이 클라이언트가 작업 공간을 선택하고, cptr이 이 모델로 응답을 생성합니다.", + "admin.gateway.modelFallback": "작업 공간/기본 모델 사용", + "admin.gateway.modelSaveError": "게이트웨이 모델 저장에 실패했습니다", + "admin.gateway.newKey": "새 API 키가 생성되었습니다. 지금 복사하세요", + "admin.gateway.noKeys": "API 키가 아직 없습니다", + "admin.gateway.title": "API 게이트웨이", + "chat.greeting": "무엇을 도와드릴까요?", + + "admin.subagents": "서브 에이전트", + "admin.subagentsEnabled": "서브 에이전트 활성화", + "admin.subagentsHint": "AI가 서브 에이전트에 작업을 위임할 수 있게 합니다. 각 서브 에이전트는 전체 도구 액세스 권한이 있는 실제 채팅을 생성합니다. 추가 LLM 호출을 사용합니다.", + "admin.subagentsMaxConcurrent": "최대 동시 실행", + "admin.subagentsMaxConcurrentHint": "동시 서브 에이전트", + "admin.subagentsMaxIterations": "최대 반복 횟수", + "admin.subagentsMaxIterationsHint": "서브 에이전트당 도구 루프", + "admin.subagentsMaxOutput": "최대 출력", + "admin.subagentsSystemPrompt": "시스템 프롬프트", + "admin.subagentsSystemPromptPlaceholder": "당신은 서브 에이전트입니다...", + "admin.subagentsSystemPromptHint": "기본값을 사용하려면 비워두세요." } diff --git a/cptr/frontend/src/lib/i18n/locales/pt-BR.json b/cptr/frontend/src/lib/i18n/locales/pt-BR.json index fcf57e6..ce689a0 100644 --- a/cptr/frontend/src/lib/i18n/locales/pt-BR.json +++ b/cptr/frontend/src/lib/i18n/locales/pt-BR.json @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. Todos os direitos reservados.", "about.updateAvailable": "v{{version}} disponível", "connections.failedToUpdate": "Falha ao atualizar conexão", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", + "keyboard.newFile": "Abrir um novo arquivo vazio", + "keyboard.newTerminal": "Abrir uma nova sessão de terminal", + "keyboard.newChat": "Iniciar um novo chat de IA", + "keyboard.closeTab": "Fechar a aba ativa", + "keyboard.nextTab": "Mudar para a próxima aba", + "keyboard.prevTab": "Mudar para a aba anterior", + "keyboard.quickOpen": "Pesquisar e abrir arquivos rapidamente", + "keyboard.openSettings": "Abrir o painel de configurações", + "keyboard.toggleSplit": "Alternar visualização dividida do editor", + "keyboard.toggleSidebar": "Mostrar ou ocultar a barra lateral", + "automations.title": "Automações", + "automations.filter": "Filtrar...", + "automations.noMatches": "Sem resultados", + "automations.noAutomations": "Nenhuma automação ainda", + "automations.create": "Criar automação", "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "automations.active": "Ativa", + "automations.paused": "Pausada", + "automations.all": "Todas", + "automations.schedule": "Agendamento", + "automations.model": "Modelo", + "automations.nextRun": "Próxima execução", + "automations.lastRun": "Última execução", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", + "automations.enabled": "Habilitado", + "automations.regenerate": "Regenerar", + "automations.disable": "Desativar", + "automations.enable": "Ativar", + "automations.copied": "Copiado!", + "automations.copy": "Copiar", "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.runs": "Execuções", + "automations.noRuns": "Nenhuma execução ainda", + "automations.viewChat": "ver chat →", + "automations.deleted": "Excluída", + "automations.webhookEnabled": "Webhook habilitado", + "automations.webhookDisabled": "Webhook desabilitado", + "automations.failedToLoad": "Falha ao carregar automações", + "automations.failedToLoadOne": "Falha ao carregar automação", + "automations.failedToToggle": "Falha ao alternar", + "automations.failedToDelete": "Falha ao excluir", + "automations.failedToRun": "Falha ao executar", + "automations.failedToGenerateWebhook": "Falha ao gerar webhook", + "automations.failedToRevokeWebhook": "Falha ao revogar webhook", + "automations.triggered": "\"{{name}}\" executada", + "automations.deleteConfirm": "Excluir \"{{name}}\"?", + "automations.runNow": "Executar agora", + "automations.newAutomation": "Nova automação", + "automations.toggleSidebar": "Alternar barra lateral", + "automationModal.titlePlaceholder": "Título da automação", + "automationModal.instructions": "Instruções", + "automationModal.promptPlaceholder": "Digite o prompt aqui...", + "automationModal.selectWorkspace": "Selecionar workspace", + "automationModal.cancel": "Cancelar", + "automationModal.saving": "Salvando...", + "automationModal.save": "Salvar", + "automationModal.createBtn": "Criar", + "automationModal.failedToSave": "Falha ao salvar automação", + "common.cancel": "Cancelar", + "common.save": "Salvar", + "common.edit": "Editar", + "common.copy": "Copiar", + "common.remove": "Remover", + "common.close": "Fechar", + "common.dismiss": "Dispensar", + "common.loading": "Carregando", + "common.downloadCsv": "Baixar CSV", + "chat.tool.readFile": "Ler {{path}}", + "chat.tool.readFileRange": "Ler {{path}} L{{range}}", + "chat.tool.editFile": "Editar {{path}}", + "chat.tool.multiEditFile": "Edição múltipla {{path}}", + "chat.tool.createFile": "Criar {{path}}", + "chat.tool.writeFile": "Escrever {{path}}", + "chat.tool.listDirectory": "Listar {{path}}", + "chat.tool.listDirectoryRecursive": "Listar {{path}} (recursivo)", + "chat.tool.searchFiles": "Buscar \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " em {{include}}", + "chat.tool.backgroundCommand": "Segundo plano: {{command}}", + "chat.tool.checkTask": "Verificar tarefa {{id}}", + "chat.tool.killTask": "Encerrar tarefa {{id}}", + "chat.tool.webSearch": "Busca web: \"{{query}}\"", + "chat.tool.fetchUrl": "Buscar {{hostname}}", + "chat.tool.fetchUrlFallback": "Buscar URL", + "chat.saveAs": "Salvar como", + "chat.send": "Enviar", + "chat.artifact": "Artefato", + "chat.exploring": "Explorando", + "chat.explored": "Explorado", + "chat.allow": "Permitir", + "chat.deny": "Negar", + "chat.toolInput": "Entrada", + "chat.toolOutput": "Saída", + "chat.toolLines": "Linhas {{start}}–{{end}}", + "chat.totalChars": "{{count}} caracteres no total", + "chat.placeholder": "Pergunte qualquer coisa sobre {{name}}...", + "chat.prevResponse": "Resposta anterior", + "chat.nextResponse": "Próxima resposta", + "chat.editResponse": "Editar resposta", + "chat.copyResponse": "Copiar resposta", + "chat.regenerateResponse": "Regenerar resposta", + "chat.usageInfo": "Informações de uso", + "chat.toggleToolCalls": "Alternar chamadas de ferramentas", + "chat.scrollToBottom": "Rolar para o final", + "chat.removeUpload": "Remover upload", + "chat.sendNow": "Enviar agora", + "chat.prevMessage": "Mensagem anterior", + "chat.nextMessage": "Próxima mensagem", + "chat.editMessage": "Editar mensagem", + "chat.copyMessage": "Copiar mensagem", + "chat.editMessagePlaceholder": "Texto da mensagem...", + "chat.editReasoningPlaceholder": "Texto do raciocínio...", + "plusMenu.askApproval": "Pedir aprovação", + "plusMenu.askApprovalDesc": "Confirmar cada chamada de ferramenta antes da execução", + "plusMenu.autoApprove": "Aprovação automática", + "plusMenu.autoApproveDesc": "Aprovar ações seguras, perguntar para as arriscadas", + "plusMenu.fullAccess": "Acesso total", + "plusMenu.fullAccessDesc": "Todas as chamadas executam sem confirmação", + "plusMenu.attachFiles": "Anexar arquivos", + "plusMenu.capture": "Capturar", + "plusMenu.planMode": "Modo planejamento", + "plusMenu.toolPermissions": "Permissões de ferramentas", + "plusMenu.parameters": "Parâmetros", + "plusMenu.noParams": "Nenhum parâmetro configurado", + "plusMenu.paramKey": "chave", + "plusMenu.paramValue": "valor", + "plusMenu.removeParam": "Remover parâmetro", + "plusMenu.addParam": "Adicionar parâmetro", + "voiceMemo.microphoneError": "Não foi possível acessar o microfone", + "voiceMemo.uploadFailed": "Áudio salvo localmente. Falha no upload para o servidor.", + "voiceMemo.sttNotConfigured": "STT não configurado. Configure em Configurações → Áudio.", + "voiceMemo.writeFailed": "Falha ao escrever a transcrição. O áudio está seguro.", + "voiceMemo.done": "Concluído →", + "voiceMemo.processing": "Processando…", + "voiceMemo.filename": "Nome do arquivo", + "voiceMemo.filenamePlaceholder": "nome-gravação", + "voiceMemo.saved": "Salvo ✓", + "admin.audio.title": "Áudio", + "admin.audio.voiceMemos": "Notas de voz", + "admin.audio.enableVoiceMemos": "Ativar notas de voz", + "admin.audio.voiceMemosHint": "Grave notas de voz pelo menu \"+\".", + "admin.audio.autoTranscribe": "Transcrição automática", + "admin.audio.transcribeOnHint": "As gravações são transcritas para markdown via STT.", + "admin.audio.transcribeOffHint": "As gravações são salvas apenas como áudio.", + "admin.audio.recordingQuality": "Qualidade da gravação", + "admin.audio.qualityHigh": "Alta (128kbps)", + "admin.audio.qualityMedium": "Média (64kbps)", + "admin.audio.qualityLow": "Baixa (32kbps)", + "admin.audio.qualityHintHigh": "Melhor qualidade, arquivos maiores.", + "admin.audio.qualityHintMedium": "Qualidade e tamanho equilibrados.", + "admin.audio.qualityHintLow": "Arquivos menores, otimizado para voz.", + "admin.audio.stt": "Voz para texto", + "admin.audio.sttHint": "Compatível com a API audio/transcriptions da OpenAI.", + "admin.audio.saveFailed": "Falha ao salvar as configurações de áudio", + "sqlite.loadError": "Falha ao carregar o banco de dados.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Executar ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} de {{total}}", + "sqlite.prev": "← Anterior", + "sqlite.next": "Próximo →", + "preview.htmlTitle": "Pré-visualização HTML", + "preview.emptySheet": "Planilha vazia", + "port.cannotConnect": "Não é possível conectar", + "connections.optional": "Opcional", + "connections.leaveBlankPlaceholder": "•••••••• (deixe em branco para manter)", + "a11y.dismissNotification": "Dispensar notificação", + "a11y.chatOptions": "Opções do chat", + "a11y.prevPage": "Página anterior", + "a11y.nextPage": "Próxima página", + "a11y.splitEditor": "Dividir editor", + "a11y.closePane": "Fechar painel", + "a11y.refreshChanges": "Atualizar alterações", + "keyboard.clickToRebind": "Clique para reatribuir", + "bar.splitRight": "Dividir à direita", + "bar.splitDown": "Dividir abaixo", + "bar.voiceMemo": "Nota de voz", + "bar.files": "Arquivos", + "messaging.noBots": "Nenhum bot de mensagens configurado.", + "messaging.failedToLoad": "Falha ao carregar os bots", + "messaging.failedToToggle": "Falha ao alternar o bot", + "messaging.failedToDelete": "Falha ao excluir o bot", + "messaging.editBot": "Editar Bot", + "messaging.addBot": "Adicionar Bot", + "messaging.name": "Nome", + "messaging.platform": "Plataforma", + "messaging.token": "Token", + "messaging.tokenKeep": "Deixe vazio para manter", + "messaging.tokenPaste": "Colar token", + "messaging.verify": "Verificar", + "messaging.allowedSenders": "Remetentes permitidos", + "messaging.allowedSendersHint": "IDs de usuários separados por vírgula. Deixe vazio para permitir todos.", + "messaging.save": "Salvar →", + "messaging.add": "Adicionar →", + "messaging.failedToSave": "Falha ao salvar", + "messaging.hint.telegram": "Criar um bot via @BotFather", + "messaging.hint.discord": "Criar um bot no Portal do Desenvolvedor", + "messaging.hint.slack": "Bot Token | App Token (separados por pipe)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (separados por pipe)", + "messaging.hint.signal": "URL do signal-cli | Telefone (separados por pipe)", + "admin.gateway.keys": "Chaves", + "admin.gateway.copy": "Copiar", + "admin.gateway.baseUrl": "URL base:", + "admin.gateway.apiKey": "Chave API:", + "admin.gateway.headers": "Cabeçalhos:", + "admin.messaging": "Mensagens", + "admin.gateway.tab": "Gateway", + "keyboard.action.newFile": "Novo arquivo", + "keyboard.action.newTerminal": "Novo terminal", + "keyboard.action.newChat": "Novo chat", + "keyboard.action.closeTab": "Fechar aba", + "keyboard.action.nextTab": "Próxima aba", + "keyboard.action.prevTab": "Aba anterior", + "keyboard.action.quickOpen": "Abertura rápida", + "keyboard.action.searchAll": "Pesquisar", + "keyboard.action.openSettings": "Abrir configurações", + "keyboard.action.toggleSplit": "Alternar divisão", + "keyboard.action.toggleSidebar": "Alternar barra lateral", + "keyboard.action.voiceMemo": "Nota de voz", + "keyboard.conflict": "Também atribuído a {{action}}", + "chat.history.justNow": "Agora", + "chat.history.minutesAgo": "há {{count}}min", + "chat.history.hoursAgo": "há {{count}}h", + "chat.history.daysAgo": "há {{count}}d", + "chat.history.title": "Título", + "chat.history.updated": "Atualizado", + "chat.history.delete": "Excluir", + "chat.edit.text": "Texto", + "chat.edit.thought": "Pensamento", + "chat.edit.tool": "Ferramenta", + "chat.edit.item": "Item", + "chat.dictate.unsupported": "O reconhecimento de voz não é suportado neste navegador.", + "chat.fallbackTitle": "Chat", + "admin.gateway.copied": "Copiado para a área de transferência", + "admin.gateway.createError": "Falha ao criar a chave", + "admin.gateway.createKey": "Criar chave", + "admin.gateway.deleteError": "Falha ao excluir a chave", + "admin.gateway.description": "Gere chaves API para conectar o Open WebUI ou qualquer cliente compatível com OpenAI aos seus workspaces.", + "admin.gateway.howToConnect": "Conectar do Open WebUI", + "admin.gateway.keyCreated": "Chave API criada", + "admin.gateway.keyDeleted": "Chave API excluída", + "admin.gateway.keyNamePlaceholder": "Nome da chave (ex. open-webui)", + "admin.gateway.keyWarning": "Esta chave será exibida apenas uma vez. Guarde-a com segurança.", + "admin.gateway.loadError": "Falha ao carregar as chaves API", + "admin.gateway.model": "Modelo de resposta", + "admin.gateway.modelDescription": "Clientes gateway escolhem um workspace; cptr usa este modelo para gerar a resposta.", + "admin.gateway.modelFallback": "Usar modelo do workspace/padrão", + "admin.gateway.modelSaveError": "Falha ao salvar o modelo gateway", + "admin.gateway.newKey": "Nova chave API criada. Copie-a agora", + "admin.gateway.noKeys": "Nenhuma chave API ainda", + "admin.gateway.title": "API Gateway", + "chat.greeting": "Como posso ajudar?", + + "admin.subagents": "Sub-agentes", + "admin.subagentsEnabled": "Habilitar sub-agentes", + "admin.subagentsHint": "Permite que a IA delegue tarefas para sub-agentes. Cada sub-agente cria um chat real com acesso total às ferramentas. Usa chamadas LLM adicionais.", + "admin.subagentsMaxConcurrent": "Máx. simultâneos", + "admin.subagentsMaxConcurrentHint": "sub-agentes simultâneos", + "admin.subagentsMaxIterations": "Máx. iterações", + "admin.subagentsMaxIterationsHint": "loops de ferramentas por sub-agente", + "admin.subagentsMaxOutput": "Máx. saída", + "admin.subagentsSystemPrompt": "Prompt do sistema", + "admin.subagentsSystemPromptPlaceholder": "Você é um sub-agente...", + "admin.subagentsSystemPromptHint": "Deixe vazio para o padrão integrado." } diff --git a/cptr/frontend/src/lib/i18n/locales/ru.json b/cptr/frontend/src/lib/i18n/locales/ru.json index 2c01255..5a149ea 100644 --- a/cptr/frontend/src/lib/i18n/locales/ru.json +++ b/cptr/frontend/src/lib/i18n/locales/ru.json @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. Все права защищены.", "about.updateAvailable": "v{{version}} доступна", "connections.failedToUpdate": "Не удалось обновить подключение", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", - "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", - "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "keyboard.newFile": "Открыть новый пустой файл", + "keyboard.newTerminal": "Открыть новую сессию терминала", + "keyboard.newChat": "Начать новый ИИ-чат", + "keyboard.closeTab": "Закрыть активную вкладку", + "keyboard.nextTab": "Перейти к следующей вкладке", + "keyboard.prevTab": "Перейти к предыдущей вкладке", + "keyboard.quickOpen": "Быстрый поиск и открытие файлов", + "keyboard.openSettings": "Открыть панель настроек", + "keyboard.toggleSplit": "Переключить разделённый вид редактора", + "keyboard.toggleSidebar": "Показать или скрыть боковую панель", + "automations.title": "Автоматизации", + "automations.filter": "Фильтр...", + "automations.noMatches": "Нет совпадений", + "automations.noAutomations": "Автоматизаций пока нет", + "automations.create": "Создать автоматизацию", + "automations.status": "Статус", + "automations.active": "Активна", + "automations.paused": "Приостановлена", + "automations.all": "Все", + "automations.schedule": "Расписание", + "automations.model": "Модель", + "automations.nextRun": "Следующий запуск", + "automations.lastRun": "Последний запуск", + "automations.webhook": "Вебхук", + "automations.enabled": "Включено", + "automations.regenerate": "Перегенерировать", + "automations.disable": "Отключить", + "automations.enable": "Включить", + "automations.copied": "Скопировано!", + "automations.copy": "Копировать", + "automations.prompt": "Промпт", + "automations.runs": "Запуски", + "automations.noRuns": "Запусков пока нет", + "automations.viewChat": "открыть чат →", + "automations.deleted": "Удалено", + "automations.webhookEnabled": "Вебхук включён", + "automations.webhookDisabled": "Вебхук отключён", + "automations.failedToLoad": "Не удалось загрузить автоматизации", + "automations.failedToLoadOne": "Не удалось загрузить автоматизацию", + "automations.failedToToggle": "Не удалось переключить", + "automations.failedToDelete": "Не удалось удалить", + "automations.failedToRun": "Не удалось запустить", + "automations.failedToGenerateWebhook": "Не удалось сгенерировать вебхук", + "automations.failedToRevokeWebhook": "Не удалось отозвать вебхук", + "automations.triggered": "\"{{name}}\" запущена", + "automations.deleteConfirm": "Удалить \"{{name}}\"?", + "automations.runNow": "Запустить сейчас", + "automations.newAutomation": "Новая автоматизация", + "automations.toggleSidebar": "Переключить боковую панель", + "automationModal.titlePlaceholder": "Название автоматизации", + "automationModal.instructions": "Инструкции", + "automationModal.promptPlaceholder": "Введите промпт...", + "automationModal.selectWorkspace": "Выбрать рабочее пространство", + "automationModal.cancel": "Отмена", + "automationModal.saving": "Сохранение...", + "automationModal.save": "Сохранить", + "automationModal.createBtn": "Создать", + "automationModal.failedToSave": "Не удалось сохранить автоматизацию", + "common.cancel": "Отмена", + "common.save": "Сохранить", + "common.edit": "Редактировать", + "common.copy": "Копировать", + "common.remove": "Удалить", + "common.close": "Закрыть", + "common.dismiss": "Закрыть", + "common.loading": "Загрузка", + "common.downloadCsv": "Скачать CSV", + "chat.tool.readFile": "Чтение {{path}}", + "chat.tool.readFileRange": "Чтение {{path}} L{{range}}", + "chat.tool.editFile": "Редактирование {{path}}", + "chat.tool.multiEditFile": "Множественное редактирование {{path}}", + "chat.tool.createFile": "Создание {{path}}", + "chat.tool.writeFile": "Запись {{path}}", + "chat.tool.listDirectory": "Список {{path}}", + "chat.tool.listDirectoryRecursive": "Список {{path}} (рекурсивно)", + "chat.tool.searchFiles": "Поиск \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " в {{include}}", + "chat.tool.backgroundCommand": "Фоновый: {{command}}", + "chat.tool.checkTask": "Проверить задачу {{id}}", + "chat.tool.killTask": "Завершить задачу {{id}}", + "chat.tool.webSearch": "Поиск в сети: \"{{query}}\"", + "chat.tool.fetchUrl": "Получить {{hostname}}", + "chat.tool.fetchUrlFallback": "Получить URL", + "chat.saveAs": "Сохранить как", + "chat.send": "Отправить", + "chat.artifact": "Артефакт", + "chat.exploring": "Исследование", + "chat.explored": "Исследовано", + "chat.allow": "Разрешить", + "chat.deny": "Отклонить", + "chat.toolInput": "Ввод", + "chat.toolOutput": "Вывод", + "chat.toolLines": "Строки {{start}}–{{end}}", + "chat.totalChars": "{{count}} символов всего", + "chat.placeholder": "Спросите что-нибудь о {{name}}...", + "chat.prevResponse": "Предыдущий ответ", + "chat.nextResponse": "Следующий ответ", + "chat.editResponse": "Редактировать ответ", + "chat.copyResponse": "Копировать ответ", + "chat.regenerateResponse": "Перегенерировать ответ", + "chat.usageInfo": "Информация об использовании", + "chat.toggleToolCalls": "Переключить вызовы инструментов", + "chat.scrollToBottom": "Прокрутить вниз", + "chat.removeUpload": "Удалить загрузку", + "chat.sendNow": "Отправить сейчас", + "chat.prevMessage": "Предыдущее сообщение", + "chat.nextMessage": "Следующее сообщение", + "chat.editMessage": "Редактировать сообщение", + "chat.copyMessage": "Копировать сообщение", + "chat.editMessagePlaceholder": "Текст сообщения...", + "chat.editReasoningPlaceholder": "Текст рассуждения...", + "plusMenu.askApproval": "Запросить одобрение", + "plusMenu.askApprovalDesc": "Подтверждать каждый вызов инструмента перед выполнением", + "plusMenu.autoApprove": "Автоодобрение", + "plusMenu.autoApproveDesc": "Одобрять безопасные действия, спрашивать о рискованных", + "plusMenu.fullAccess": "Полный доступ", + "plusMenu.fullAccessDesc": "Все вызовы выполняются без подтверждения", + "plusMenu.attachFiles": "Прикрепить файлы", + "plusMenu.capture": "Захват", + "plusMenu.planMode": "Режим планирования", + "plusMenu.toolPermissions": "Разрешения инструментов", + "plusMenu.parameters": "Параметры", + "plusMenu.noParams": "Параметры не настроены", + "plusMenu.paramKey": "ключ", + "plusMenu.paramValue": "значение", + "plusMenu.removeParam": "Удалить параметр", + "plusMenu.addParam": "Добавить параметр", + "voiceMemo.microphoneError": "Не удалось получить доступ к микрофону", + "voiceMemo.uploadFailed": "Аудио сохранено локально. Загрузка на сервер не удалась.", + "voiceMemo.sttNotConfigured": "STT не настроен. Настройте в Настройки → Аудио.", + "voiceMemo.writeFailed": "Не удалось записать транскрипцию. Аудио в безопасности.", + "voiceMemo.done": "Готово →", + "voiceMemo.processing": "Обработка…", + "voiceMemo.filename": "Имя файла", + "voiceMemo.filenamePlaceholder": "название-записи", + "voiceMemo.saved": "Сохранено ✓", + "admin.audio.title": "Аудио", + "admin.audio.voiceMemos": "Голосовые заметки", + "admin.audio.enableVoiceMemos": "Включить голосовые заметки", + "admin.audio.voiceMemosHint": "Записывайте голосовые заметки из меню \"+\".", + "admin.audio.autoTranscribe": "Автоматическая транскрипция", + "admin.audio.transcribeOnHint": "Записи транскрибируются в markdown через STT.", + "admin.audio.transcribeOffHint": "Записи сохраняются только как аудио.", + "admin.audio.recordingQuality": "Качество записи", + "admin.audio.qualityHigh": "Высокое (128кбит/с)", + "admin.audio.qualityMedium": "Среднее (64кбит/с)", + "admin.audio.qualityLow": "Низкое (32кбит/с)", + "admin.audio.qualityHintHigh": "Лучшее качество, большие файлы.", + "admin.audio.qualityHintMedium": "Сбалансированное качество и размер.", + "admin.audio.qualityHintLow": "Наименьшие файлы, оптимизировано для речи.", + "admin.audio.stt": "Распознавание речи", + "admin.audio.sttHint": "Совместимо с API audio/transcriptions OpenAI.", + "admin.audio.saveFailed": "Не удалось сохранить настройки аудио", + "sqlite.loadError": "Не удалось загрузить базу данных.", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}Б]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "Выполнить ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} из {{total}}", + "sqlite.prev": "← Назад", + "sqlite.next": "Далее →", + "preview.htmlTitle": "Предпросмотр HTML", + "preview.emptySheet": "Пустой лист", + "port.cannotConnect": "Не удается подключиться", + "connections.optional": "Необязательно", + "connections.leaveBlankPlaceholder": "•••••••• (оставьте пустым для сохранения)", + "a11y.dismissNotification": "Закрыть уведомление", + "a11y.chatOptions": "Параметры чата", + "a11y.prevPage": "Предыдущая страница", + "a11y.nextPage": "Следующая страница", + "a11y.splitEditor": "Разделить редактор", + "a11y.closePane": "Закрыть панель", + "a11y.refreshChanges": "Обновить изменения", + "keyboard.clickToRebind": "Нажмите для переназначения", + "bar.splitRight": "Разделить вправо", + "bar.splitDown": "Разделить вниз", + "bar.voiceMemo": "Голосовая заметка", + "bar.files": "Файлы", + "messaging.noBots": "Боты для обмена сообщениями не настроены.", + "messaging.failedToLoad": "Не удалось загрузить ботов", + "messaging.failedToToggle": "Не удалось переключить бота", + "messaging.failedToDelete": "Не удалось удалить бота", + "messaging.editBot": "Редактировать бота", + "messaging.addBot": "Добавить бота", + "messaging.name": "Имя", + "messaging.platform": "Платформа", + "messaging.token": "Токен", + "messaging.tokenKeep": "Оставьте пустым для сохранения", + "messaging.tokenPaste": "Вставить токен", + "messaging.verify": "Проверить", + "messaging.allowedSenders": "Разрешённые отправители", + "messaging.allowedSendersHint": "ID пользователей через запятую. Оставьте пустым для разрешения всем.", + "messaging.save": "Сохранить →", + "messaging.add": "Добавить →", + "messaging.failedToSave": "Не удалось сохранить", + "messaging.hint.telegram": "Создайте бота через @BotFather", + "messaging.hint.discord": "Создайте бота на портале разработчиков", + "messaging.hint.slack": "Bot Token | App Token (через pipe)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (через pipe)", + "messaging.hint.signal": "URL signal-cli | Номер телефона (через pipe)", + "admin.gateway.keys": "Ключи", + "admin.gateway.copy": "Копировать", + "admin.gateway.baseUrl": "Базовый URL:", + "admin.gateway.apiKey": "API-ключ:", + "admin.gateway.headers": "Заголовки:", + "admin.messaging": "Сообщения", + "admin.gateway.tab": "Шлюз", + "keyboard.action.newFile": "Новый файл", + "keyboard.action.newTerminal": "Новый терминал", + "keyboard.action.newChat": "Новый чат", + "keyboard.action.closeTab": "Закрыть вкладку", + "keyboard.action.nextTab": "Следующая вкладка", + "keyboard.action.prevTab": "Предыдущая вкладка", + "keyboard.action.quickOpen": "Быстрое открытие", + "keyboard.action.searchAll": "Поиск", + "keyboard.action.openSettings": "Открыть настройки", + "keyboard.action.toggleSplit": "Переключить разделение", + "keyboard.action.toggleSidebar": "Переключить боковую панель", + "keyboard.action.voiceMemo": "Голосовая заметка", + "keyboard.conflict": "Также назначено на {{action}}", + "chat.history.justNow": "Только что", + "chat.history.minutesAgo": "{{count}}мин назад", + "chat.history.hoursAgo": "{{count}}ч назад", + "chat.history.daysAgo": "{{count}}д назад", + "chat.history.title": "Название", + "chat.history.updated": "Обновлено", + "chat.history.delete": "Удалить", + "chat.edit.text": "Текст", + "chat.edit.thought": "Мысль", + "chat.edit.tool": "Инструмент", + "chat.edit.item": "Элемент", + "chat.dictate.unsupported": "Распознавание речи не поддерживается в этом браузере.", + "chat.fallbackTitle": "Чат", + "admin.gateway.copied": "Скопировано в буфер обмена", + "admin.gateway.createError": "Не удалось создать ключ", + "admin.gateway.createKey": "Создать ключ", + "admin.gateway.deleteError": "Не удалось удалить ключ", + "admin.gateway.description": "Генерируйте API-ключи для подключения Open WebUI или любого совместимого с OpenAI клиента к вашим рабочим пространствам.", + "admin.gateway.howToConnect": "Подключение из Open WebUI", + "admin.gateway.keyCreated": "API-ключ создан", + "admin.gateway.keyDeleted": "API-ключ удалён", + "admin.gateway.keyNamePlaceholder": "Имя ключа (напр. open-webui)", + "admin.gateway.keyWarning": "Этот ключ будет показан только один раз. Сохраните его надёжно.", + "admin.gateway.loadError": "Не удалось загрузить API-ключи", + "admin.gateway.model": "Модель ответа", + "admin.gateway.modelDescription": "Клиенты шлюза выбирают рабочее пространство; cptr использует эту модель для генерации ответа.", + "admin.gateway.modelFallback": "Использовать модель рабочего пространства/по умолчанию", + "admin.gateway.modelSaveError": "Не удалось сохранить модель шлюза", + "admin.gateway.newKey": "Новый API-ключ создан. Скопируйте его сейчас", + "admin.gateway.noKeys": "API-ключей пока нет", + "admin.gateway.title": "API-шлюз", + "chat.greeting": "Чем могу помочь?", + + "admin.subagents": "Суб-агенты", + "admin.subagentsEnabled": "Включить суб-агентов", + "admin.subagentsHint": "Позволяет ИИ делегировать задачи суб-агентам. Каждый суб-агент создаёт реальный чат с полным доступом к инструментам. Использует дополнительные вызовы LLM.", + "admin.subagentsMaxConcurrent": "Макс. одновременно", + "admin.subagentsMaxConcurrentHint": "одновременных суб-агентов", + "admin.subagentsMaxIterations": "Макс. итераций", + "admin.subagentsMaxIterationsHint": "циклов инструментов на суб-агента", + "admin.subagentsMaxOutput": "Макс. вывод", + "admin.subagentsSystemPrompt": "Системный промпт", + "admin.subagentsSystemPromptPlaceholder": "Вы — суб-агент...", + "admin.subagentsSystemPromptHint": "Оставьте пустым для значения по умолчанию." } diff --git a/cptr/frontend/src/lib/i18n/locales/zh-CN.json b/cptr/frontend/src/lib/i18n/locales/zh-CN.json index 3433a9f..4efb68d 100644 --- a/cptr/frontend/src/lib/i18n/locales/zh-CN.json +++ b/cptr/frontend/src/lib/i18n/locales/zh-CN.json @@ -235,7 +235,7 @@ "system.disk": "磁盘", "system.cores": "{{count}} 核", "system.process": "进程", - "admin.web": "网页", + "admin.web": "网络", "admin.webEnabled": "启用网页访问", "admin.webEnabledHint": "AI 可以搜索网页并获取 URL。", "admin.webDisabledHint": "网页搜索和 URL 获取已禁用。", @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. 保留所有权利。", "about.updateAvailable": "v{{version}} 可用", "connections.failedToUpdate": "更新连接失败", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "keyboard.newFile": "打开新的空白文件", + "keyboard.newTerminal": "打开新的终端会话", + "keyboard.newChat": "开始新的 AI 聊天", + "keyboard.closeTab": "关闭活动标签页", + "keyboard.nextTab": "切换到下一个标签页", + "keyboard.prevTab": "切换到上一个标签页", + "keyboard.quickOpen": "快速搜索并打开文件", + "keyboard.openSettings": "打开设置面板", + "keyboard.toggleSplit": "切换编辑器分割视图", + "keyboard.toggleSidebar": "显示或隐藏侧边栏", + "automations.title": "自动化", + "automations.filter": "筛选...", + "automations.noMatches": "无匹配", + "automations.noAutomations": "暂无自动化", + "automations.create": "创建自动化", + "automations.status": "状态", + "automations.active": "活动", + "automations.paused": "已暂停", + "automations.all": "全部", + "automations.schedule": "计划", + "automations.model": "模型", + "automations.nextRun": "下次运行", + "automations.lastRun": "上次运行", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", - "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.enabled": "已启用", + "automations.regenerate": "重新生成", + "automations.disable": "禁用", + "automations.enable": "启用", + "automations.copied": "已复制!", + "automations.copy": "复制", + "automations.prompt": "提示词", + "automations.runs": "运行记录", + "automations.noRuns": "暂无运行记录", + "automations.viewChat": "查看对话 →", + "automations.deleted": "已删除", + "automations.webhookEnabled": "Webhook 已启用", + "automations.webhookDisabled": "Webhook 已禁用", + "automations.failedToLoad": "加载自动化失败", + "automations.failedToLoadOne": "加载自动化失败", + "automations.failedToToggle": "切换失败", + "automations.failedToDelete": "删除失败", + "automations.failedToRun": "运行失败", + "automations.failedToGenerateWebhook": "生成 Webhook 失败", + "automations.failedToRevokeWebhook": "撤销 Webhook 失败", + "automations.triggered": "\"{{name}}\" 已触发", + "automations.deleteConfirm": "删除 \"{{name}}\"?", + "automations.runNow": "立即运行", + "automations.newAutomation": "新建自动化", + "automations.toggleSidebar": "切换侧边栏", + "automationModal.titlePlaceholder": "自动化标题", + "automationModal.instructions": "说明", + "automationModal.promptPlaceholder": "在此输入提示词...", + "automationModal.selectWorkspace": "选择工作区", + "automationModal.cancel": "取消", + "automationModal.saving": "保存中...", + "automationModal.save": "保存", + "automationModal.createBtn": "创建", + "automationModal.failedToSave": "保存自动化失败", + "common.cancel": "取消", + "common.save": "保存", + "common.edit": "编辑", + "common.copy": "复制", + "common.remove": "移除", + "common.close": "关闭", + "common.dismiss": "忽略", + "common.loading": "加载中", + "common.downloadCsv": "下载 CSV", + "chat.tool.readFile": "读取 {{path}}", + "chat.tool.readFileRange": "读取 {{path}} L{{range}}", + "chat.tool.editFile": "编辑 {{path}}", + "chat.tool.multiEditFile": "多处编辑 {{path}}", + "chat.tool.createFile": "创建 {{path}}", + "chat.tool.writeFile": "写入 {{path}}", + "chat.tool.listDirectory": "列出 {{path}}", + "chat.tool.listDirectoryRecursive": "列出 {{path}} (递归)", + "chat.tool.searchFiles": "搜索 \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " 在 {{include}} 中", + "chat.tool.backgroundCommand": "后台: {{command}}", + "chat.tool.checkTask": "检查任务 {{id}}", + "chat.tool.killTask": "终止任务 {{id}}", + "chat.tool.webSearch": "网络搜索: \"{{query}}\"", + "chat.tool.fetchUrl": "获取 {{hostname}}", + "chat.tool.fetchUrlFallback": "获取 URL", + "chat.saveAs": "另存为", + "chat.send": "发送", + "chat.artifact": "工件", + "chat.exploring": "探索中", + "chat.explored": "已探索", + "chat.allow": "允许", + "chat.deny": "拒绝", + "chat.toolInput": "输入", + "chat.toolOutput": "输出", + "chat.toolLines": "第 {{start}}–{{end}} 行", + "chat.totalChars": "共 {{count}} 个字符", + "chat.placeholder": "关于 {{name}} 的任何问题...", + "chat.prevResponse": "上一个回复", + "chat.nextResponse": "下一个回复", + "chat.editResponse": "编辑回复", + "chat.copyResponse": "复制回复", + "chat.regenerateResponse": "重新生成回复", + "chat.usageInfo": "使用信息", + "chat.toggleToolCalls": "切换工具调用显示", + "chat.scrollToBottom": "滚动到底部", + "chat.removeUpload": "移除上传", + "chat.sendNow": "立即发送", + "chat.prevMessage": "上一条消息", + "chat.nextMessage": "下一条消息", + "chat.editMessage": "编辑消息", + "chat.copyMessage": "复制消息", + "chat.editMessagePlaceholder": "消息文本...", + "chat.editReasoningPlaceholder": "推理文本...", + "plusMenu.askApproval": "请求批准", + "plusMenu.askApprovalDesc": "每次工具调用前确认", + "plusMenu.autoApprove": "自动批准", + "plusMenu.autoApproveDesc": "批准安全操作,询问风险操作", + "plusMenu.fullAccess": "完全访问", + "plusMenu.fullAccessDesc": "所有工具调用无需确认即可执行", + "plusMenu.attachFiles": "附加文件", + "plusMenu.capture": "截图", + "plusMenu.planMode": "计划模式", + "plusMenu.toolPermissions": "工具权限", + "plusMenu.parameters": "参数", + "plusMenu.noParams": "未配置参数", + "plusMenu.paramKey": "键", + "plusMenu.paramValue": "值", + "plusMenu.removeParam": "移除参数", + "plusMenu.addParam": "添加参数", + "voiceMemo.microphoneError": "无法访问麦克风", + "voiceMemo.uploadFailed": "音频已保存到本地。服务器上传失败。", + "voiceMemo.sttNotConfigured": "STT 未配置。请在设置 → 音频中配置。", + "voiceMemo.writeFailed": "无法写入转录文本。音频已安全保存。", + "voiceMemo.done": "完成 →", + "voiceMemo.processing": "处理中…", + "voiceMemo.filename": "文件名", + "voiceMemo.filenamePlaceholder": "录音名称", + "voiceMemo.saved": "已保存 ✓", + "admin.audio.title": "音频", + "admin.audio.voiceMemos": "语音备忘录", + "admin.audio.enableVoiceMemos": "启用语音备忘录", + "admin.audio.voiceMemosHint": "从菜单录制语音备忘录。", + "admin.audio.autoTranscribe": "自动转录", + "admin.audio.transcribeOnHint": "录音通过 STT 转录为 Markdown。", + "admin.audio.transcribeOffHint": "录音仅保存为音频。", + "admin.audio.recordingQuality": "录音质量", + "admin.audio.qualityHigh": "高质量 (128kbps)", + "admin.audio.qualityMedium": "中等 (64kbps)", + "admin.audio.qualityLow": "低质量 (32kbps)", + "admin.audio.qualityHintHigh": "最佳质量,文件较大。", + "admin.audio.qualityHintMedium": "质量与大小均衡。", + "admin.audio.qualityHintLow": "最小文件,针对语音优化。", + "admin.audio.stt": "语音转文字", + "admin.audio.sttHint": "兼容 OpenAI 的 audio/transcriptions API。", + "admin.audio.saveFailed": "保存音频设置失败", + "sqlite.loadError": "无法加载数据库。", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "运行 ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} / {{total}}", + "sqlite.prev": "← 上一页", + "sqlite.next": "下一页 →", + "preview.htmlTitle": "HTML 预览", + "preview.emptySheet": "空表格", + "port.cannotConnect": "无法连接", + "connections.optional": "可选", + "connections.leaveBlankPlaceholder": "•••••••• (留空以保留当前值)", + "a11y.dismissNotification": "关闭通知", + "a11y.chatOptions": "聊天选项", + "a11y.prevPage": "上一页", + "a11y.nextPage": "下一页", + "a11y.splitEditor": "分割编辑器", + "a11y.closePane": "关闭面板", + "a11y.refreshChanges": "刷新更改", + "keyboard.clickToRebind": "点击重新绑定", + "bar.splitRight": "向右分割", + "bar.splitDown": "向下分割", + "bar.voiceMemo": "语音备忘录", + "bar.files": "文件", + "messaging.noBots": "未配置消息机器人。", + "messaging.failedToLoad": "加载机器人失败", + "messaging.failedToToggle": "切换机器人失败", + "messaging.failedToDelete": "删除机器人失败", + "messaging.editBot": "编辑机器人", + "messaging.addBot": "添加机器人", + "messaging.name": "名称", + "messaging.platform": "平台", + "messaging.token": "令牌", + "messaging.tokenKeep": "留空以保留当前值", + "messaging.tokenPaste": "粘贴令牌", + "messaging.verify": "验证", + "messaging.allowedSenders": "允许的发送者", + "messaging.allowedSendersHint": "逗号分隔的用户 ID。留空允许所有人。", + "messaging.save": "保存 →", + "messaging.add": "添加 →", + "messaging.failedToSave": "保存失败", + "messaging.hint.telegram": "通过 @BotFather 创建机器人", + "messaging.hint.discord": "在开发者门户创建机器人", + "messaging.hint.slack": "Bot Token | App Token (竖线分隔)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (竖线分隔)", + "messaging.hint.signal": "signal-cli URL | 电话号码 (竖线分隔)", + "admin.gateway.keys": "密钥", + "admin.gateway.copy": "复制", + "admin.gateway.baseUrl": "基础 URL:", + "admin.gateway.apiKey": "API 密钥:", + "admin.gateway.headers": "请求头:", + "admin.messaging": "消息", + "admin.gateway.tab": "网关", + "keyboard.action.newFile": "新建文件", + "keyboard.action.newTerminal": "新建终端", + "keyboard.action.newChat": "新建聊天", + "keyboard.action.closeTab": "关闭标签页", + "keyboard.action.nextTab": "下一个标签页", + "keyboard.action.prevTab": "上一个标签页", + "keyboard.action.quickOpen": "快速打开", + "keyboard.action.searchAll": "搜索", + "keyboard.action.openSettings": "打开设置", + "keyboard.action.toggleSplit": "切换分割", + "keyboard.action.toggleSidebar": "切换侧边栏", + "keyboard.action.voiceMemo": "语音备忘录", + "keyboard.conflict": "同时绑定到 {{action}}", + "chat.history.justNow": "刚刚", + "chat.history.minutesAgo": "{{count}}分钟前", + "chat.history.hoursAgo": "{{count}}小时前", + "chat.history.daysAgo": "{{count}}天前", + "chat.history.title": "标题", + "chat.history.updated": "已更新", + "chat.history.delete": "删除", + "chat.edit.text": "文本", + "chat.edit.thought": "思考", + "chat.edit.tool": "工具", + "chat.edit.item": "项目", + "chat.dictate.unsupported": "此浏览器不支持语音识别。", + "chat.fallbackTitle": "聊天", + "admin.gateway.copied": "已复制到剪贴板", + "admin.gateway.createError": "创建密钥失败", + "admin.gateway.createKey": "创建密钥", + "admin.gateway.deleteError": "删除密钥失败", + "admin.gateway.description": "生成 API 密钥,将 Open WebUI 或任何兼容 OpenAI 的客户端连接到您的工作区。", + "admin.gateway.howToConnect": "从 Open WebUI 连接", + "admin.gateway.keyCreated": "API 密钥已创建", + "admin.gateway.keyDeleted": "API 密钥已删除", + "admin.gateway.keyNamePlaceholder": "密钥名称(例如 open-webui)", + "admin.gateway.keyWarning": "此密钥只会显示一次。请安全保存。", + "admin.gateway.loadError": "加载 API 密钥失败", + "admin.gateway.model": "响应模型", + "admin.gateway.modelDescription": "网关客户端选择工作区;cptr 使用此模型生成响应。", + "admin.gateway.modelFallback": "使用工作区/默认模型", + "admin.gateway.modelSaveError": "保存网关模型失败", + "admin.gateway.newKey": "新 API 密钥已创建。请立即复制", + "admin.gateway.noKeys": "暂无 API 密钥", + "admin.gateway.title": "API 网关", + "chat.greeting": "有什么可以帮您的?", + + "admin.subagents": "子代理", + "admin.subagentsEnabled": "启用子代理", + "admin.subagentsHint": "允许 AI 将任务委托给子代理。每个子代理会创建一个拥有完整工具访问权限的真实聊天。会使用额外的 LLM 调用。", + "admin.subagentsMaxConcurrent": "最大并发数", + "admin.subagentsMaxConcurrentHint": "个同时运行的子代理", + "admin.subagentsMaxIterations": "最大迭代次数", + "admin.subagentsMaxIterationsHint": "每个子代理的工具循环次数", + "admin.subagentsMaxOutput": "最大输出", + "admin.subagentsSystemPrompt": "系统提示词", + "admin.subagentsSystemPromptPlaceholder": "你是一个子代理...", + "admin.subagentsSystemPromptHint": "留空以使用内置默认值。" } diff --git a/cptr/frontend/src/lib/i18n/locales/zh-TW.json b/cptr/frontend/src/lib/i18n/locales/zh-TW.json index 4f313cf..6b54bf6 100644 --- a/cptr/frontend/src/lib/i18n/locales/zh-TW.json +++ b/cptr/frontend/src/lib/i18n/locales/zh-TW.json @@ -235,7 +235,7 @@ "system.disk": "磁碟", "system.cores": "{{count}} 核心", "system.process": "處理程序", - "admin.web": "網頁", + "admin.web": "網路", "admin.webEnabled": "啟用網頁存取", "admin.webEnabledHint": "AI 可以搜尋網頁並擷取 URL。", "admin.webDisabledHint": "網頁搜尋與 URL 擷取已停用。", @@ -317,62 +317,269 @@ "about.copyright": "Copyright © 2026 Open WebUI Inc. 保留所有權利。", "about.updateAvailable": "v{{version}} 可用", "connections.failedToUpdate": "更新連線失敗", - "keyboard.newFile": "Open a new untitled file", - "keyboard.newTerminal": "Open a new terminal session", - "keyboard.newChat": "Start a new AI chat", - "keyboard.closeTab": "Close the active tab", - "keyboard.nextTab": "Switch to the next tab", - "keyboard.prevTab": "Switch to the previous tab", - "keyboard.quickOpen": "Search and open files quickly", - "keyboard.openSettings": "Open the settings panel", - "keyboard.toggleSplit": "Toggle split editor view", - "keyboard.toggleSidebar": "Show or hide the sidebar", - "automations.title": "Automations", - "automations.filter": "Filter...", - "automations.noMatches": "No matches", - "automations.noAutomations": "No automations yet", - "automations.create": "Create automation", - "automations.status": "Status", - "automations.active": "Active", - "automations.paused": "Paused", - "automations.all": "All", - "automations.schedule": "Schedule", - "automations.model": "Model", - "automations.nextRun": "Next run", - "automations.lastRun": "Last run", + "keyboard.newFile": "開啟新的空白檔案", + "keyboard.newTerminal": "開啟新的終端機工作階段", + "keyboard.newChat": "開始新的 AI 聊天", + "keyboard.closeTab": "關閉使用中的分頁", + "keyboard.nextTab": "切換到下一個分頁", + "keyboard.prevTab": "切換到上一個分頁", + "keyboard.quickOpen": "快速搜尋並開啟檔案", + "keyboard.openSettings": "開啟設定面板", + "keyboard.toggleSplit": "切換編輯器分割檢視", + "keyboard.toggleSidebar": "顯示或隱藏側邊欄", + "automations.title": "自動化", + "automations.filter": "篩選...", + "automations.noMatches": "無符合項目", + "automations.noAutomations": "尚無自動化", + "automations.create": "建立自動化", + "automations.status": "狀態", + "automations.active": "活動中", + "automations.paused": "已暫停", + "automations.all": "全部", + "automations.schedule": "排程", + "automations.model": "模型", + "automations.nextRun": "下次執行", + "automations.lastRun": "上次執行", "automations.webhook": "Webhook", - "automations.enabled": "Enabled", - "automations.regenerate": "Regenerate", - "automations.disable": "Disable", - "automations.enable": "Enable", - "automations.copied": "Copied!", - "automations.copy": "Copy", - "automations.prompt": "Prompt", - "automations.runs": "Runs", - "automations.noRuns": "No runs yet", - "automations.viewChat": "view chat →", - "automations.deleted": "Deleted", - "automations.webhookEnabled": "Webhook enabled", - "automations.webhookDisabled": "Webhook disabled", - "automations.failedToLoad": "Failed to load automations", - "automations.failedToLoadOne": "Failed to load automation", - "automations.failedToToggle": "Failed to toggle", - "automations.failedToDelete": "Failed to delete", - "automations.failedToRun": "Failed to run", - "automations.failedToGenerateWebhook": "Failed to generate webhook", - "automations.failedToRevokeWebhook": "Failed to revoke webhook", - "automations.triggered": "\"{{name}}\" triggered", - "automations.deleteConfirm": "Delete \"{{name}}\"?", - "automations.runNow": "Run now", - "automations.newAutomation": "New automation", - "automations.toggleSidebar": "Toggle sidebar", - "automationModal.titlePlaceholder": "Automation title", - "automationModal.instructions": "Instructions", - "automationModal.promptPlaceholder": "Enter prompt here...", - "automationModal.selectWorkspace": "Select workspace", - "automationModal.cancel": "Cancel", - "automationModal.saving": "Saving...", - "automationModal.save": "Save", - "automationModal.createBtn": "Create", - "automationModal.failedToSave": "Failed to save automation" + "automations.enabled": "已啟用", + "automations.regenerate": "重新產生", + "automations.disable": "停用", + "automations.enable": "啟用", + "automations.copied": "已複製!", + "automations.copy": "複製", + "automations.prompt": "提示詞", + "automations.runs": "執行記錄", + "automations.noRuns": "尚無執行記錄", + "automations.viewChat": "檢視對話 →", + "automations.deleted": "已刪除", + "automations.webhookEnabled": "Webhook 已啟用", + "automations.webhookDisabled": "Webhook 已停用", + "automations.failedToLoad": "載入自動化失敗", + "automations.failedToLoadOne": "載入自動化失敗", + "automations.failedToToggle": "切換失敗", + "automations.failedToDelete": "刪除失敗", + "automations.failedToRun": "執行失敗", + "automations.failedToGenerateWebhook": "產生 Webhook 失敗", + "automations.failedToRevokeWebhook": "撤銷 Webhook 失敗", + "automations.triggered": "\"{{name}}\" 已觸發", + "automations.deleteConfirm": "刪除 \"{{name}}\"?", + "automations.runNow": "立即執行", + "automations.newAutomation": "新增自動化", + "automations.toggleSidebar": "切換側邊欄", + "automationModal.titlePlaceholder": "自動化標題", + "automationModal.instructions": "說明", + "automationModal.promptPlaceholder": "在此輸入提示詞...", + "automationModal.selectWorkspace": "選擇工作區", + "automationModal.cancel": "取消", + "automationModal.saving": "儲存中...", + "automationModal.save": "儲存", + "automationModal.createBtn": "建立", + "automationModal.failedToSave": "儲存自動化失敗", + "common.cancel": "取消", + "common.save": "儲存", + "common.edit": "編輯", + "common.copy": "複製", + "common.remove": "移除", + "common.close": "關閉", + "common.dismiss": "忽略", + "common.loading": "載入中", + "common.downloadCsv": "下載 CSV", + "chat.tool.readFile": "讀取 {{path}}", + "chat.tool.readFileRange": "讀取 {{path}} L{{range}}", + "chat.tool.editFile": "編輯 {{path}}", + "chat.tool.multiEditFile": "多處編輯 {{path}}", + "chat.tool.createFile": "建立 {{path}}", + "chat.tool.writeFile": "寫入 {{path}}", + "chat.tool.listDirectory": "列出 {{path}}", + "chat.tool.listDirectoryRecursive": "列出 {{path}} (遞迴)", + "chat.tool.searchFiles": "搜尋 \"{{query}}\"{{scope}}", + "chat.tool.searchFilesScope": " 在 {{include}} 中", + "chat.tool.backgroundCommand": "背景: {{command}}", + "chat.tool.checkTask": "檢查工作 {{id}}", + "chat.tool.killTask": "終止工作 {{id}}", + "chat.tool.webSearch": "網路搜尋: \"{{query}}\"", + "chat.tool.fetchUrl": "取得 {{hostname}}", + "chat.tool.fetchUrlFallback": "取得 URL", + "chat.saveAs": "另存為", + "chat.send": "傳送", + "chat.artifact": "成品", + "chat.exploring": "探索中", + "chat.explored": "已探索", + "chat.allow": "允許", + "chat.deny": "拒絕", + "chat.toolInput": "輸入", + "chat.toolOutput": "輸出", + "chat.toolLines": "第 {{start}}–{{end}} 行", + "chat.totalChars": "共 {{count}} 個字元", + "chat.placeholder": "關於 {{name}} 的任何問題...", + "chat.prevResponse": "上一個回覆", + "chat.nextResponse": "下一個回覆", + "chat.editResponse": "編輯回覆", + "chat.copyResponse": "複製回覆", + "chat.regenerateResponse": "重新產生回覆", + "chat.usageInfo": "使用資訊", + "chat.toggleToolCalls": "切換工具呼叫顯示", + "chat.scrollToBottom": "捲動至底部", + "chat.removeUpload": "移除上傳", + "chat.sendNow": "立即傳送", + "chat.prevMessage": "上一則訊息", + "chat.nextMessage": "下一則訊息", + "chat.editMessage": "編輯訊息", + "chat.copyMessage": "複製訊息", + "chat.editMessagePlaceholder": "訊息文字...", + "chat.editReasoningPlaceholder": "推理文字...", + "plusMenu.askApproval": "請求核准", + "plusMenu.askApprovalDesc": "每次工具呼叫前確認", + "plusMenu.autoApprove": "自動核准", + "plusMenu.autoApproveDesc": "核准安全操作,詢問風險操作", + "plusMenu.fullAccess": "完整存取", + "plusMenu.fullAccessDesc": "所有工具呼叫無需確認即可執行", + "plusMenu.attachFiles": "附加檔案", + "plusMenu.capture": "擷取", + "plusMenu.planMode": "計畫模式", + "plusMenu.toolPermissions": "工具權限", + "plusMenu.parameters": "參數", + "plusMenu.noParams": "未設定參數", + "plusMenu.paramKey": "鍵", + "plusMenu.paramValue": "值", + "plusMenu.removeParam": "移除參數", + "plusMenu.addParam": "新增參數", + "voiceMemo.microphoneError": "無法存取麥克風", + "voiceMemo.uploadFailed": "音訊已儲存至本機。伺服器上傳失敗。", + "voiceMemo.sttNotConfigured": "STT 未設定。請在設定 → 音訊中設定。", + "voiceMemo.writeFailed": "無法寫入轉錄文字。音訊已安全儲存。", + "voiceMemo.done": "完成 →", + "voiceMemo.processing": "處理中…", + "voiceMemo.filename": "檔案名稱", + "voiceMemo.filenamePlaceholder": "錄音名稱", + "voiceMemo.saved": "已儲存 ✓", + "admin.audio.title": "音訊", + "admin.audio.voiceMemos": "語音備忘錄", + "admin.audio.enableVoiceMemos": "啟用語音備忘錄", + "admin.audio.voiceMemosHint": "從「+」選單錄製語音備忘錄。", + "admin.audio.autoTranscribe": "自動轉錄", + "admin.audio.transcribeOnHint": "錄音透過 STT 轉錄為 Markdown。", + "admin.audio.transcribeOffHint": "錄音僅儲存為音訊。", + "admin.audio.recordingQuality": "錄音品質", + "admin.audio.qualityHigh": "高品質 (128kbps)", + "admin.audio.qualityMedium": "中等 (64kbps)", + "admin.audio.qualityLow": "低品質 (32kbps)", + "admin.audio.qualityHintHigh": "最佳品質,檔案較大。", + "admin.audio.qualityHintMedium": "品質與大小均衡。", + "admin.audio.qualityHintLow": "最小檔案,針對語音最佳化。", + "admin.audio.stt": "語音轉文字", + "admin.audio.sttHint": "相容 OpenAI 的 audio/transcriptions API。", + "admin.audio.saveFailed": "儲存音訊設定失敗", + "sqlite.loadError": "無法載入資料庫。", + "sqlite.null": "NULL", + "sqlite.blob": "[BLOB {{size}}B]", + "sqlite.sql": "SQL", + "sqlite.queryPlaceholder": "SELECT * FROM ...", + "sqlite.run": "執行 ⌘↵", + "sqlite.pageInfo": "{{start}}–{{end}} / {{total}}", + "sqlite.prev": "← 上一頁", + "sqlite.next": "下一頁 →", + "preview.htmlTitle": "HTML 預覽", + "preview.emptySheet": "空白工作表", + "port.cannotConnect": "無法連線", + "connections.optional": "選填", + "connections.leaveBlankPlaceholder": "•••••••• (留空以保留目前的值)", + "a11y.dismissNotification": "關閉通知", + "a11y.chatOptions": "聊天選項", + "a11y.prevPage": "上一頁", + "a11y.nextPage": "下一頁", + "a11y.splitEditor": "分割編輯器", + "a11y.closePane": "關閉面板", + "a11y.refreshChanges": "重新整理變更", + "keyboard.clickToRebind": "點擊重新繫結", + "bar.splitRight": "向右分割", + "bar.splitDown": "向下分割", + "bar.voiceMemo": "語音備忘錄", + "bar.files": "檔案", + "messaging.noBots": "未設定訊息機器人。", + "messaging.failedToLoad": "載入機器人失敗", + "messaging.failedToToggle": "切換機器人失敗", + "messaging.failedToDelete": "刪除機器人失敗", + "messaging.editBot": "編輯機器人", + "messaging.addBot": "新增機器人", + "messaging.name": "名稱", + "messaging.platform": "平台", + "messaging.token": "權杖", + "messaging.tokenKeep": "留空以保留目前的值", + "messaging.tokenPaste": "貼上權杖", + "messaging.verify": "驗證", + "messaging.allowedSenders": "允許的傳送者", + "messaging.allowedSendersHint": "逗號分隔的使用者 ID。留空允許所有人。", + "messaging.save": "儲存 →", + "messaging.add": "新增 →", + "messaging.failedToSave": "儲存失敗", + "messaging.hint.telegram": "透過 @BotFather 建立機器人", + "messaging.hint.discord": "在開發者入口網站建立機器人", + "messaging.hint.slack": "Bot Token | App Token (管線符號分隔)", + "messaging.hint.whatsapp": "Access Token | Phone Number ID (管線符號分隔)", + "messaging.hint.signal": "signal-cli URL | 電話號碼 (管線符號分隔)", + "admin.gateway.keys": "金鑰", + "admin.gateway.copy": "複製", + "admin.gateway.baseUrl": "基礎 URL:", + "admin.gateway.apiKey": "API 金鑰:", + "admin.gateway.headers": "標頭:", + "admin.messaging": "訊息", + "admin.gateway.tab": "閘道", + "keyboard.action.newFile": "新增檔案", + "keyboard.action.newTerminal": "新增終端機", + "keyboard.action.newChat": "新增聊天", + "keyboard.action.closeTab": "關閉分頁", + "keyboard.action.nextTab": "下一個分頁", + "keyboard.action.prevTab": "上一個分頁", + "keyboard.action.quickOpen": "快速開啟", + "keyboard.action.searchAll": "搜尋", + "keyboard.action.openSettings": "開啟設定", + "keyboard.action.toggleSplit": "切換分割", + "keyboard.action.toggleSidebar": "切換側邊欄", + "keyboard.action.voiceMemo": "語音備忘錄", + "keyboard.conflict": "同時繫結到 {{action}}", + "chat.history.justNow": "剛才", + "chat.history.minutesAgo": "{{count}}分鐘前", + "chat.history.hoursAgo": "{{count}}小時前", + "chat.history.daysAgo": "{{count}}天前", + "chat.history.title": "標題", + "chat.history.updated": "已更新", + "chat.history.delete": "刪除", + "chat.edit.text": "文字", + "chat.edit.thought": "思考", + "chat.edit.tool": "工具", + "chat.edit.item": "項目", + "chat.dictate.unsupported": "此瀏覽器不支援語音辨識。", + "chat.fallbackTitle": "聊天", + "admin.gateway.copied": "已複製到剪貼簿", + "admin.gateway.createError": "建立金鑰失敗", + "admin.gateway.createKey": "建立金鑰", + "admin.gateway.deleteError": "刪除金鑰失敗", + "admin.gateway.description": "產生 API 金鑰,將 Open WebUI 或任何相容 OpenAI 的客戶端連接到您的工作區。", + "admin.gateway.howToConnect": "從 Open WebUI 連線", + "admin.gateway.keyCreated": "API 金鑰已建立", + "admin.gateway.keyDeleted": "API 金鑰已刪除", + "admin.gateway.keyNamePlaceholder": "金鑰名稱(例如 open-webui)", + "admin.gateway.keyWarning": "此金鑰只會顯示一次。請安全儲存。", + "admin.gateway.loadError": "載入 API 金鑰失敗", + "admin.gateway.model": "回應模型", + "admin.gateway.modelDescription": "閘道客戶端選擇工作區;cptr 使用此模型產生回應。", + "admin.gateway.modelFallback": "使用工作區/預設模型", + "admin.gateway.modelSaveError": "儲存閘道模型失敗", + "admin.gateway.newKey": "新 API 金鑰已建立。請立即複製", + "admin.gateway.noKeys": "尚無 API 金鑰", + "admin.gateway.title": "API 閘道", + "chat.greeting": "有什麼可以幫您的?", + + "admin.subagents": "子代理", + "admin.subagentsEnabled": "啟用子代理", + "admin.subagentsHint": "允許 AI 將任務委派給子代理。每個子代理會建立一個擁有完整工具存取權限的真實聊天。會使用額外的 LLM 呼叫。", + "admin.subagentsMaxConcurrent": "最大同時數", + "admin.subagentsMaxConcurrentHint": "個同時執行的子代理", + "admin.subagentsMaxIterations": "最大迭代次數", + "admin.subagentsMaxIterationsHint": "每個子代理的工具迴圈次數", + "admin.subagentsMaxOutput": "最大輸出", + "admin.subagentsSystemPrompt": "系統提示詞", + "admin.subagentsSystemPromptPlaceholder": "你是一個子代理...", + "admin.subagentsSystemPromptHint": "留空以使用內建預設值。" } diff --git a/cptr/routers/admin.py b/cptr/routers/admin.py index 7ab2436..effd75e 100644 --- a/cptr/routers/admin.py +++ b/cptr/routers/admin.py @@ -425,3 +425,170 @@ async def update_model_config( await Config.upsert({CONFIG_KEY_CHAT_MODELS: all_config}) return {"ok": True} + +# ── Tool servers ───────────────────────────────────────────── + +CONFIG_KEY_TOOL_SERVERS = "tool_servers" + + +async def _get_tool_servers() -> list[dict]: + """Get all tool server configs from config store.""" + return await Config.get(CONFIG_KEY_TOOL_SERVERS) or [] + + +async def _save_tool_servers(servers: list[dict]): + """Save tool server configs and invalidate cache.""" + await Config.upsert({CONFIG_KEY_TOOL_SERVERS: servers}) + from cptr.utils.tools import invalidate_tool_server_cache + + invalidate_tool_server_cache() + + +def _mask_tool_server(server: dict) -> dict: + """Return server with masked API key for display.""" + masked = {**server} + if masked.get("key"): + key = masked["key"] + masked["key"] = key[:4] + "****" + key[-4:] if len(key) > 8 else "****" + return masked + + +@router.get("/tools/servers") +async def list_tool_servers(request: Request): + """List all configured tool servers (keys masked).""" + require_admin(request) + servers = await _get_tool_servers() + return {"servers": [_mask_tool_server(s) for s in servers]} + + +class CreateToolServerRequest(BaseModel): + id: str + type: str = "openapi" # "openapi" | "mcp" + url: str + path: str = "openapi.json" # OpenAPI spec path (OpenAPI only) + auth_type: str = "bearer" # "bearer" | "none" + key: Optional[str] = None + name: str = "" + description: str = "" + headers: Optional[dict] = None + enabled: bool = True + + +@router.post("/tools/servers") +async def create_tool_server(body: CreateToolServerRequest, request: Request): + """Add a new external tool server.""" + require_admin(request) + import re as _re + + server_id = body.id.strip() + if not server_id or not _re.fullmatch(r"[a-z0-9_]+", server_id): + raise HTTPException(400, "ID must be lowercase alphanumeric with underscores only") + + servers = await _get_tool_servers() + if any(s["id"] == server_id for s in servers): + raise HTTPException(409, f"Server ID '{server_id}' already exists") + + server = { + "id": server_id, + "type": body.type, + "url": body.url, + "path": body.path, + "auth_type": body.auth_type, + "key": body.key or "", + "name": body.name or server_id, + "description": body.description, + "headers": body.headers, + "enabled": body.enabled, + } + servers.append(server) + await _save_tool_servers(servers) + return {"ok": True, "id": server["id"]} + + +class UpdateToolServerRequest(BaseModel): + type: Optional[str] = None + url: Optional[str] = None + path: Optional[str] = None + auth_type: Optional[str] = None + key: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None + headers: Optional[dict] = None + enabled: Optional[bool] = None + + +@router.put("/tools/servers/{server_id}") +async def update_tool_server(server_id: str, body: UpdateToolServerRequest, request: Request): + """Update an existing tool server.""" + require_admin(request) + servers = await _get_tool_servers() + server = next((s for s in servers if s["id"] == server_id), None) + if not server: + raise HTTPException(404, "tool server not found") + + for field in ("type", "url", "path", "auth_type", "name", "description"): + val = getattr(body, field) + if val is not None: + server[field] = val + if body.key is not None and body.key != "": + server["key"] = body.key + if body.headers is not None: + server["headers"] = body.headers + if body.enabled is not None: + server["enabled"] = body.enabled + + await _save_tool_servers(servers) + return {"ok": True} + + +@router.delete("/tools/servers/{server_id}") +async def delete_tool_server(server_id: str, request: Request): + """Delete a tool server.""" + require_admin(request) + servers = [s for s in await _get_tool_servers() if s["id"] != server_id] + await _save_tool_servers(servers) + return {"ok": True} + + +@router.post("/tools/servers/{server_id}/verify") +async def verify_tool_server(server_id: str, request: Request): + """Test connectivity to a tool server. Returns discovered tools.""" + require_admin(request) + servers = await _get_tool_servers() + server = next((s for s in servers if s["id"] == server_id), None) + if not server: + raise HTTPException(404, "tool server not found") + + server_type = server.get("type", "openapi") + url = server.get("url", "") + headers = dict(server.get("headers") or {}) + if server.get("auth_type") == "bearer" and server.get("key"): + headers["Authorization"] = f"Bearer {server['key']}" + + try: + if server_type == "mcp": + from cptr.utils.mcp.client import MCPClient + + client = MCPClient() + await client.connect(url, headers or None) + try: + specs = await client.list_tool_specs() + return {"ok": True, "tools": specs} + finally: + await client.disconnect() + + else: # openapi + from cptr.utils.openapi import fetch_openapi_spec, convert_openapi_to_tool_specs + + path = server.get("path", "openapi.json") + if path.startswith("http"): + spec_url = path + else: + spec_url = f"{url.rstrip('/')}/{path.lstrip('/')}" + + spec = await fetch_openapi_spec(spec_url, headers or None) + tools = convert_openapi_to_tool_specs(spec) + return {"ok": True, "tools": tools} + + except Exception as e: + return JSONResponse({"ok": False, "message": str(e)}, 400) diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index fa699c7..556de3b 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -26,7 +26,7 @@ ) from cptr.utils.config import _get_jwt_secret, now_ms from cptr.utils.crypto import decrypt_key -from cptr.utils.tools import TOOLS, execute_tool, get_tool_list, _fn_to_schema, create_artifact +from cptr.utils.tools import ALL_TOOLS, execute_tool, get_tool_list, _fn_to_schema, create_artifact from cptr.utils.chat_export import export_chat_to_file from cptr.utils.json_parser import extract_json @@ -861,8 +861,12 @@ def _sync_state(): if not skills: tools = [t for t in tools if t["name"] != "view_skill"] - # Parse $skill-name mentions from the user message to auto-activate skills + # Strip delegate_task from sub-agent chats (depth limit = 1) chat_obj = await Chat.get_by_id(chat_id) + if chat_obj and (chat_obj.meta or {}).get("subagent"): + tools = [t for t in tools if t["name"] != "delegate_task"] + + # Parse $skill-name mentions from the user message to auto-activate skills chat_params = (chat_obj.meta or {}).get("params", {}) if chat_obj else {} attached_skill_ids: list[str] = [] if skills and messages: @@ -891,7 +895,7 @@ def _sync_state(): # Plan mode: strip write tools, inject prompt as user message (not system, to preserve cache) plan_mode = chat_params.get("plan_mode", False) if plan_mode: - tools = [t for t in tools if TOOLS.get(t["name"], {}).get("auto")] + tools = [t for t in tools if ALL_TOOLS.get(t["name"], {}).get("auto")] # Inject create_artifact (only available in plan mode) tools.append(_fn_to_schema("create_artifact", create_artifact)) messages.append({"role": "user", "content": PLAN_MODE_PROMPT}) @@ -1024,6 +1028,7 @@ def _sync_state(): ) restart = False + pending_calls: list[dict] = [] # Collect tool calls from this response async for event in stream: if event["type"] == "text_delta": @@ -1033,93 +1038,8 @@ def _sync_state(): _sync_state() elif event["type"] == "tool_call": - # Flush any text before the tool call - flushed_item = _flush_text() - - name = event["name"] - tool = TOOLS.get(name) - item = { - "type": "function_call", - "id": str(uuid.uuid4()), - "call_id": event["call_id"], - "fc_id": event.get("id", ""), - "name": name, - "arguments": event["arguments"], - } - - should_auto = approval_mode == "full" or ( - approval_mode == "auto" and tool and tool["auto"] - ) - - if should_auto: - # Show tool call in progress BEFORE execution - item["status"] = "in_progress" - output_items.append(item) - if flushed_item: - await emit(output=flushed_item) - await emit(output=item) - _sync_state() - - if name == "create_artifact": - result = await create_artifact( - **event["arguments"], workspace=workspace - ) - else: - result = await execute_tool( - name, - event["arguments"], - { - "workspace": workspace, - "user_id": user_id, - "model_id": model, - "chat_id": chat_id, - }, - ) - - # Update status to completed - item["status"] = "completed" - result_item = { - "type": "function_call_output", - "call_id": event["call_id"], - "output": result, - } - output_items.append(result_item) - await emit(output=item) - await emit(output=result_item) - _sync_state() - - # Artifact UI card: detect create_artifact or create_file with artifact_type - artifact_item = build_artifact_item(name, event["arguments"], result) - if artifact_item: - output_items.append(artifact_item) - await emit(output=artifact_item) - _sync_state() - - # Persist intermediate state so content survives crashes/errors - await ChatMessage.update(message_id, content=content, output=output_items) - - # Append to messages for next iteration - _append_tool_to_messages(messages, event, result, provider) - new_messages_since += 2 # tool_call + tool_result - restart = True - break - - else: - # Needs approval, persist and stop - item["status"] = "pending" - output_items.append(item) - await ChatMessage.update( - message_id, - content=content, - output=output_items, - done=False, - ) - if flushed_item: - await emit(output=flushed_item) - await emit(output=item) - _task_state.pop(message_id, None) - await emit(done=True) - return + # Collect tool call — don't execute yet + pending_calls.append(event) elif event["type"] == "usage": _flush_text() @@ -1130,27 +1050,174 @@ def _sync_state(): ) last_usage = usage new_messages_since = 0 - logger.info( - "[task %s] save (usage): content=%d chars, output=%d items, types=%s", - message_id[:8], - len(content), - len(output_items), - [i.get("type") for i in output_items], + + if not pending_calls: + # No tool calls — final response, we're done + logger.info( + "[task %s] save (usage): content=%d chars, output=%d items, types=%s", + message_id[:8], + len(content), + len(output_items), + [i.get("type") for i in output_items], + ) + await ChatMessage.update( + message_id, + content=content, + output=output_items, + usage=usage, + done=True, + ) + _task_state.pop(message_id, None) + await _emit_done() + return + + elif event["type"] == "done": + # Stream ended without explicit usage + pass + + # ── Process collected tool calls ──────────────────── + if pending_calls: + flushed_item = _flush_text() + + tool_ctx = { + "workspace": workspace, + "user_id": user_id, + "model_id": model, + "chat_id": chat_id, + "connection": connection, + } + + # Check if any call needs approval + needs_approval = None + for tc in pending_calls: + name = tc["name"] + tool = ALL_TOOLS.get(name) + should_auto = approval_mode == "full" or ( + approval_mode == "auto" and tool and tool["auto"] ) + if not should_auto: + needs_approval = tc + break + + if needs_approval: + # First non-auto tool stops the loop for approval + tc = needs_approval + item = { + "type": "function_call", + "id": str(uuid.uuid4()), + "call_id": tc["call_id"], + "fc_id": tc.get("id", ""), + "name": tc["name"], + "arguments": tc["arguments"], + "status": "pending", + } + output_items.append(item) await ChatMessage.update( message_id, content=content, output=output_items, - usage=usage, - done=True, + done=False, ) + if flushed_item: + await emit(output=flushed_item) + await emit(output=item) _task_state.pop(message_id, None) - await _emit_done() + await emit(done=True) return - elif event["type"] == "done": - # Stream ended without explicit usage - pass + # All calls are auto-approved — build UI items + call_items: list[tuple[dict, dict]] = [] # (event, ui_item) + for tc in pending_calls: + item = { + "type": "function_call", + "id": str(uuid.uuid4()), + "call_id": tc["call_id"], + "fc_id": tc.get("id", ""), + "name": tc["name"], + "arguments": tc["arguments"], + "status": "in_progress", + } + output_items.append(item) + call_items.append((tc, item)) + + # Emit all as in_progress + if flushed_item: + await emit(output=flushed_item) + for _, item in call_items: + await emit(output=item) + _sync_state() + + # Separate delegate_task (concurrent) from others (sequential) + delegate_indices = [i for i, (tc, _) in enumerate(call_items) if tc["name"] == "delegate_task"] + other_indices = [i for i, (tc, _) in enumerate(call_items) if tc["name"] != "delegate_task"] + + # Execute non-delegate tools sequentially first + for idx in other_indices: + tc, item = call_items[idx] + if tc["name"] == "create_artifact": + result = await create_artifact(**tc["arguments"], workspace=workspace) + else: + result = await execute_tool(tc["name"], tc["arguments"], tool_ctx) + + item["status"] = "completed" + result_item = { + "type": "function_call_output", + "call_id": tc["call_id"], + "output": result, + } + output_items.append(result_item) + await emit(output=item) + await emit(output=result_item) + _sync_state() + + artifact_item = build_artifact_item(tc["name"], tc["arguments"], result) + if artifact_item: + output_items.append(artifact_item) + await emit(output=artifact_item) + _sync_state() + + _append_tool_to_messages(messages, tc, result, provider) + new_messages_since += 2 + + # Execute delegate_task calls concurrently, emit each as it completes + if delegate_indices: + # Create tasks, mapping task → index + inflight: dict[asyncio.Task, int] = {} + for idx in delegate_indices: + tc, _ = call_items[idx] + task = asyncio.create_task( + execute_tool(tc["name"], tc["arguments"], tool_ctx) + ) + inflight[task] = idx + + while inflight: + done_set, _ = await asyncio.wait( + inflight.keys(), return_when=asyncio.FIRST_COMPLETED + ) + for task in done_set: + idx = inflight.pop(task) + tc, item = call_items[idx] + try: + result = task.result() + except Exception as e: + result = f"Error: {e}" + + item["status"] = "completed" + result_item = { + "type": "function_call_output", + "call_id": tc["call_id"], + "output": result, + } + output_items.append(result_item) + await emit(output=item) + await emit(output=result_item) + _sync_state() + _append_tool_to_messages(messages, tc, result, provider) + new_messages_since += 2 + + # Persist after all tool calls + await ChatMessage.update(message_id, content=content, output=output_items) + restart = True if not restart: flushed_item = _flush_text() diff --git a/cptr/utils/context.py b/cptr/utils/context.py index 3451f71..c0d8aca 100644 --- a/cptr/utils/context.py +++ b/cptr/utils/context.py @@ -63,11 +63,16 @@ def should_compact( def _get_threshold() -> int: - """Read threshold: config.toml > env var/default.""" + """Read threshold: app_config (admin UI) > config.toml [chat] > env var/default.""" try: from cptr.utils.config import load_config config = load_config() + # Admin UI saves to [app_config] with dotted keys + val = config.get("app_config", {}).get("chat.compact_token_threshold") + if val is not None: + return int(val) + # Manual config.toml [chat] section val = config.get("chat", {}).get("compact_token_threshold") if val is not None: return int(val) diff --git a/cptr/utils/mcp/__init__.py b/cptr/utils/mcp/__init__.py new file mode 100644 index 0000000..aaa1082 --- /dev/null +++ b/cptr/utils/mcp/__init__.py @@ -0,0 +1 @@ +# MCP client utilities diff --git a/cptr/utils/mcp/client.py b/cptr/utils/mcp/client.py new file mode 100644 index 0000000..4df3b52 --- /dev/null +++ b/cptr/utils/mcp/client.py @@ -0,0 +1,147 @@ +"""MCP (Model Context Protocol) client for Streamable HTTP transport. + +Wraps the `mcp` library's ClientSession to provide a simple interface +for connecting to MCP servers, listing tools, and calling tools. + +Usage:: + + client = MCPClient() + await client.connect("https://mcp.example.com/sse", headers={"Authorization": "Bearer ..."}) + specs = await client.list_tool_specs() + result = await client.call_tool("my_tool", {"arg": "value"}) + await client.disconnect() +""" + +from __future__ import annotations + +import asyncio +import logging +from contextlib import AsyncExitStack + +import anyio +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client +from mcp.types import CallToolResult + +logger = logging.getLogger(__name__) + +_INIT_TIMEOUT = 30 # seconds + + +class MCPClient: + """Manages a single MCP server connection over Streamable HTTP.""" + + def __init__(self): + self.session: ClientSession | None = None + self._exit_stack: AsyncExitStack | None = None + + async def connect(self, url: str, headers: dict | None = None) -> None: + """Connect to an MCP server. + + Args: + url: The server's Streamable HTTP endpoint URL. + headers: Optional HTTP headers (e.g. Authorization). + """ + async with AsyncExitStack() as exit_stack: + try: + transport = await exit_stack.enter_async_context( + streamablehttp_client(url=url, headers=headers) + ) + read, write, _ = transport + + self.session = await exit_stack.enter_async_context( + ClientSession(read_stream=read, write_stream=write) + ) + + with anyio.fail_after(_INIT_TIMEOUT): + await self.session.initialize() + + # Transfer ownership — prevent exit_stack.__aexit__ from + # tearing everything down when we leave this block. + self._exit_stack = exit_stack.pop_all() + logger.info("[mcp] Connected to %s", url) + except Exception: + await asyncio.shield(self.disconnect()) + raise + + async def list_tool_specs(self) -> list[dict]: + """List tools from the connected server as OpenAI-compatible schemas. + + Returns a list of dicts, each with: name, description, parameters. + """ + if not self.session: + raise RuntimeError("MCPClient is not connected") + + result = await self.session.list_tools() + specs = [] + + for tool in result.tools: + spec = { + "name": tool.name, + "description": tool.description or "", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + } + + if tool.inputSchema: + schema = tool.inputSchema + spec["parameters"]["properties"] = schema.get("properties", {}) + spec["parameters"]["required"] = schema.get("required", []) + + specs.append(spec) + + return specs + + async def call_tool(self, name: str, function_args: dict | None = None) -> list: + """Call a tool on the connected server. + + Args: + name: The tool name. + function_args: Arguments to pass to the tool. + + Returns: + A list of content items from the tool result. + + Raises: + RuntimeError: If the tool returns an error result. + """ + if not self.session: + raise RuntimeError("MCPClient is not connected") + + result: CallToolResult = await self.session.call_tool( + name, arguments=function_args or {} + ) + + content = [item.model_dump() for item in result.content] + + if result.isError: + raise RuntimeError(f"MCP tool error: {content}") + + return content + + async def disconnect(self) -> None: + """Disconnect from the MCP server. Idempotent. + + IMPORTANT: Do NOT use asyncio.shield() or anyio.CancelScope here. + The MCP SDK requires its TaskGroup to be exited in the same task + that created it. Simply call aclose() directly. + """ + exit_stack = self._exit_stack + if exit_stack is None: + return + + # Prevent double-close from concurrent callers + self._exit_stack = None + self.session = None + + try: + await exit_stack.aclose() + except TimeoutError: + logger.warning("[mcp] disconnect timed out") + except RuntimeError as exc: + logger.debug("[mcp] disconnect suppressed RuntimeError: %s", exc) + except Exception: + logger.debug("[mcp] Error during disconnect", exc_info=True) diff --git a/cptr/utils/openapi.py b/cptr/utils/openapi.py new file mode 100644 index 0000000..faa8725 --- /dev/null +++ b/cptr/utils/openapi.py @@ -0,0 +1,311 @@ +"""OpenAPI tool server client. + +Handles fetching OpenAPI specs, converting them to LLM-compatible tool schemas, +and executing tool calls against OpenAPI servers. +""" + +from __future__ import annotations + +import copy +import json +import logging +from typing import Any + +import httpx + +logger = logging.getLogger(__name__) + +# Valid HTTP methods per OpenAPI 3.x +_HTTP_METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"} + +_TIMEOUT = 15 # seconds + + +# ── Spec fetching ─────────────────────────────────────────── + + +async def fetch_openapi_spec( + url: str, headers: dict | None = None, timeout: float = _TIMEOUT +) -> dict: + """Fetch and parse an OpenAPI spec from a URL. + + Supports JSON and YAML (if PyYAML is installed). Falls back to YAML + parsing for non-JSON content. + + Args: + url: Full URL to the OpenAPI spec endpoint. + headers: Optional HTTP headers (e.g. Authorization). + timeout: Request timeout in seconds. + + Returns: + The parsed OpenAPI spec as a dict. + """ + _headers = {"Accept": "application/json"} + if headers: + _headers.update(headers) + + async with httpx.AsyncClient(timeout=timeout, verify=True) as client: + resp = await client.get(url, headers=_headers) + resp.raise_for_status() + + text = resp.text + if url.lower().endswith((".yaml", ".yml")): + import yaml + + return yaml.safe_load(text) + + try: + return json.loads(text) + except json.JSONDecodeError: + # Fallback to YAML for non-.yml URLs that aren't valid JSON + import yaml + + return yaml.safe_load(text) + + +# ── Schema conversion ────────────────────────────────────── + + +def _resolve_schema( + schema: dict, components: dict, resolved: set | None = None +) -> dict: + """Recursively resolve $ref references in a JSON schema.""" + if not schema: + return {} + + if resolved is None: + resolved = set() + + if "$ref" in schema: + ref_path = schema["$ref"] + schema_name = ref_path.split("/")[-1] + + if schema_name in resolved: + return {} # Avoid infinite recursion + + resolved.add(schema_name) + + ref_parts = ref_path.strip("#/").split("/") + target = components + for part in ref_parts[1:]: # Skip 'components' + target = target.get(part, {}) + return _resolve_schema(target, components, resolved) + + result = copy.deepcopy(schema) + + if "properties" in result: + for prop, prop_schema in result["properties"].items(): + result["properties"][prop] = _resolve_schema(prop_schema, components) + + if "items" in result: + result["items"] = _resolve_schema(result["items"], components) + + for keyword in ("oneOf", "anyOf", "allOf"): + if keyword in result and isinstance(result[keyword], list): + result[keyword] = [ + _resolve_schema(inner, components, resolved) for inner in result[keyword] + ] + + return result + + +def convert_openapi_to_tool_specs(openapi_spec: dict) -> list[dict]: + """Convert an OpenAPI specification into tool schemas for the LLM. + + Each operation with an ``operationId`` becomes a tool. Parameters and + request body properties are flattened into a single ``parameters`` object. + + Args: + openapi_spec: The parsed OpenAPI spec dict. + + Returns: + A list of tool spec dicts with name, description, and parameters. + """ + specs = [] + components = openapi_spec.get("components", {}) + + for path, methods in openapi_spec.get("paths", {}).items(): + if not isinstance(methods, dict): + continue + + path_params = methods.get("parameters", []) + if not isinstance(path_params, list): + path_params = [] + + for method, operation in methods.items(): + if method not in _HTTP_METHODS: + continue + if not isinstance(operation, dict): + continue + if not operation.get("operationId"): + continue + + tool = { + "name": operation["operationId"], + "description": operation.get( + "description", operation.get("summary", "No description available.") + ), + "parameters": {"type": "object", "properties": {}, "required": []}, + } + + # Merge path-level and operation-level params + op_params = operation.get("parameters", []) + if not isinstance(op_params, list): + op_params = [] + + merged = {} + for param in path_params: + if isinstance(param, dict) and param.get("name"): + merged[(param["name"], param.get("in", ""))] = param + for param in op_params: + if isinstance(param, dict) and param.get("name"): + merged[(param["name"], param.get("in", ""))] = param + + for param in merged.values(): + pname = param.get("name") + if not pname: + continue + pschema = param.get("schema", {}) + desc = pschema.get("description", "") or param.get("description", "") + if pschema.get("enum") and isinstance(pschema["enum"], list): + desc += f'. Possible values: {", ".join(str(v) for v in pschema["enum"])}' + + prop = { + "type": pschema.get("type") or "string", + "description": desc, + } + if pschema.get("type") == "array" and "items" in pschema: + prop["items"] = pschema["items"] + + prop = {k: v for k, v in prop.items() if v is not None} + tool["parameters"]["properties"][pname] = prop + if param.get("required"): + tool["parameters"]["required"].append(pname) + + # Extract requestBody + request_body = operation.get("requestBody") + if request_body: + content = request_body.get("content", {}) + json_schema = content.get("application/json", {}).get("schema") + if json_schema: + resolved = _resolve_schema(json_schema, components) + if resolved.get("properties"): + tool["parameters"]["properties"].update(resolved["properties"]) + if "required" in resolved: + tool["parameters"]["required"] = list( + set(tool["parameters"]["required"] + resolved["required"]) + ) + elif resolved.get("type") == "array": + tool["parameters"] = resolved + + specs.append(tool) + + return specs + + +# ── Tool execution ────────────────────────────────────────── + + +async def execute_openapi_tool( + server_url: str, + openapi_spec: dict, + tool_name: str, + args: dict, + headers: dict | None = None, + timeout: float = 60, +) -> str: + """Execute a tool call against an OpenAPI server. + + Resolves the route by operationId, separates path/query/body params, + and makes the appropriate HTTP request. + + Args: + server_url: Base URL of the OpenAPI server. + openapi_spec: The full parsed OpenAPI spec. + tool_name: The operationId to call. + args: Arguments from the LLM tool call. + headers: Optional HTTP headers. + timeout: Request timeout in seconds. + + Returns: + The response body as a string (JSON-serialized if structured). + """ + paths = openapi_spec.get("paths", {}) + + # Find matching route + route_path = None + http_method = None + operation = None + + for rpath, methods in paths.items(): + if not isinstance(methods, dict): + continue + for method, op in methods.items(): + if method not in _HTTP_METHODS: + continue + if isinstance(op, dict) and op.get("operationId") == tool_name: + route_path = rpath + http_method = method + operation = op + break + if route_path: + break + + if not route_path or not operation: + return json.dumps({"error": f"Operation '{tool_name}' not found in OpenAPI spec"}) + + # Classify parameters + path_params = {} + query_params = {} + body_params = dict(args) + + all_params = (operation.get("parameters") or []) + ( + paths.get(route_path, {}).get("parameters") or [] + ) + + for param in all_params: + pname = param.get("name", "") + pin = param.get("in", "") + if pname in body_params: + if pin == "path": + path_params[pname] = body_params.pop(pname) + elif pin == "query": + query_params[pname] = body_params.pop(pname) + elif pin == "header": + body_params.pop(pname) # skip header params for now + + # Build URL with path params + url = server_url.rstrip("/") + route_path + for pname, pval in path_params.items(): + url = url.replace(f"{{{pname}}}", str(pval)) + + _headers = {"Content-Type": "application/json"} + if headers: + _headers.update(headers) + + try: + async with httpx.AsyncClient(timeout=timeout, verify=True) as client: + if http_method in ("get", "head", "options"): + resp = await client.request( + http_method.upper(), url, headers=_headers, params=query_params + ) + else: + resp = await client.request( + http_method.upper(), + url, + headers=_headers, + params=query_params, + json=body_params if body_params else None, + ) + + # Try to parse as JSON + try: + data = resp.json() + return json.dumps(data, indent=2, ensure_ascii=False) + except Exception: + return resp.text + + except httpx.TimeoutException: + return json.dumps({"error": f"Request to {url} timed out after {timeout}s"}) + except Exception as e: + return json.dumps({"error": str(e)}) diff --git a/cptr/utils/tools.py b/cptr/utils/tools.py index 7ca14c1..154f3ec 100644 --- a/cptr/utils/tools.py +++ b/cptr/utils/tools.py @@ -1198,7 +1198,7 @@ async def browser_evaluate(javascript: str, *, __context__: dict) -> str: "delete_automation": {"fn": delete_automation, "auto": False}, } -# Browser tools — registered conditionally based on browser.enabled config +# Browser tools — conditionally included in schemas based on browser.enabled BROWSER_TOOLS: dict[str, dict] = { "browser_navigate": {"fn": browser_navigate, "auto": False}, "browser_snapshot": {"fn": browser_snapshot, "auto": True}, @@ -1209,6 +1209,298 @@ async def browser_evaluate(javascript: str, *, __context__: dict) -> str: } +# ── Sub-agent ─────────────────────────────────────────────── + +_DEFAULT_SUBAGENT_SYSTEM = """You are a sub-agent working on a specific task assigned by the lead agent. + +You have full access to the workspace — you can read, write, edit files, and run commands. +Focus exclusively on your assigned task. Do NOT work on anything outside your scope. + +When done, end with a clear summary: +- What you did +- What files you changed (if any) +- Any issues or open questions +""" + +_subagent_semaphore: asyncio.Semaphore | None = None + + +async def _get_subagent_config() -> dict: + """Load sub-agent settings from config with defaults.""" + from cptr.models import Config + + return { + "max_concurrent": int(await Config.get("subagents.max_concurrent") or 3), + "max_iterations": int(await Config.get("subagents.max_iterations") or 30), + "max_output": int(await Config.get("subagents.max_output") or 30_000), + "system_prompt": (await Config.get("subagents.system_prompt")) + or _DEFAULT_SUBAGENT_SYSTEM, + } + + +def _truncate_output(text: str, max_chars: int) -> str: + """Truncate text to max_chars, appending a note if truncated.""" + if len(text) <= max_chars: + return text + return text[:max_chars] + "\n\n[output truncated]" + + +async def delegate_task( + task: str, + context: str = "", + *, + __context__: dict, +) -> str: + """Delegate a task to a sub-agent. The sub-agent has full access to read, write, edit files, and run commands. Use for parallel work — call multiple times in one response to run tasks concurrently. + + :param task: What the sub-agent should do. + :param context: Optional context (e.g. relevant file paths, decisions made so far). + """ + global _subagent_semaphore + config = await _get_subagent_config() + if _subagent_semaphore is None: + _subagent_semaphore = asyncio.Semaphore(config["max_concurrent"]) + + async with _subagent_semaphore: + return await _run_subagent_chat( + task=task, + context=context, + workspace=__context__["workspace"], + connection=__context__["connection"], + model=__context__["model_id"], + user_id=__context__["user_id"], + parent_chat_id=__context__["chat_id"], + config=config, + ) + + +async def _run_subagent_chat( + task: str, + context: str, + workspace: str, + connection: dict, + model: str, + user_id: str, + parent_chat_id: str, + config: dict, +) -> str: + """Create a real chat and run the agent loop on it.""" + from cptr.models import Chat, ChatMessage + from cptr.utils.chat_task import run_chat_task + from cptr.utils.config import now_ms + + # Build the user message content + user_content = f"{task}\n\n## Context\n{context}" if context else task + + # Create a real chat, marked as a sub-agent + chat = await Chat.create( + user_id=user_id, + title=f"Sub-agent: {task[:60]}", + meta={ + "workspace": workspace, + "subagent": True, + "parent_chat_id": parent_chat_id, + "params": { + "tool_approval_mode": "full", # auto-approve all tools + }, + }, + created_at=now_ms(), + ) + + # Create user message + user_msg = await ChatMessage.create( + chat_id=chat.id, + role="user", + content=user_content, + created_at=now_ms(), + ) + + # Create empty assistant message + assistant_msg = await ChatMessage.create( + chat_id=chat.id, + role="assistant", + content="", + parent_id=user_msg.id, + model=model, + done=False, + created_at=now_ms(), + ) + + await Chat.update_current_message(chat.id, assistant_msg.id, now_ms()) + + # Run the SAME agent loop as a normal chat — no special code + await run_chat_task( + message_id=assistant_msg.id, + chat_id=chat.id, + user_id=user_id, + connection=connection, + workspace=workspace, + model=model, + ) + + # Read back the completed message + result_msg = await ChatMessage.get_by_id(assistant_msg.id) + output = result_msg.content if result_msg else "Sub-agent produced no output." + + return _truncate_output(output, config["max_output"]) + + +SUBAGENT_TOOLS: dict[str, dict] = { + "delegate_task": {"fn": delegate_task, "auto": True}, +} + +# Combined lookup for execution and approval (always available regardless of config) +ALL_TOOLS: dict[str, dict] = {**TOOLS, **BROWSER_TOOLS, **SUBAGENT_TOOLS} + + +# ── External tool servers ─────────────────────────────────── + +_tool_server_cache: dict | None = None # {"servers": [...], "tools": {name: {server, spec}}} + + +async def _load_tool_servers() -> dict: + """Load and cache external tool server config + specs. + + Returns a dict with 'servers' (raw config list) and 'tools' mapping + prefixed tool names to {server, spec, type}. + """ + global _tool_server_cache + if _tool_server_cache is not None: + return _tool_server_cache + + from cptr.models import Config + + servers = await Config.get("tool_servers") or [] + tools: dict[str, dict] = {} + + for server in servers: + if not server.get("enabled", True): + continue + + server_id = server.get("id", "") + server_type = server.get("type", "openapi") + + try: + if server_type == "openapi": + from cptr.utils.openapi import fetch_openapi_spec, convert_openapi_to_tool_specs + + url = server.get("url", "").rstrip("/") + path = server.get("path", "openapi.json") + if path.startswith("http"): + spec_url = path + else: + spec_url = f"{url}/{path.lstrip('/')}" + + headers = _build_server_headers(server) + openapi_spec = await fetch_openapi_spec(spec_url, headers) + server["_openapi_spec"] = openapi_spec + + for spec in convert_openapi_to_tool_specs(openapi_spec): + prefixed = f"{server_id}_{spec['name']}" + tools[prefixed] = { + "server": server, + "spec": {**spec, "name": prefixed}, + "original_name": spec["name"], + "type": "openapi", + } + + elif server_type == "mcp": + from cptr.utils.mcp.client import MCPClient + + client = MCPClient() + headers = _build_server_headers(server) + await client.connect(server.get("url", ""), headers) + + for spec in await client.list_tool_specs(): + prefixed = f"{server_id}_{spec['name']}" + tools[prefixed] = { + "server": server, + "spec": {**spec, "name": prefixed}, + "original_name": spec["name"], + "type": "mcp", + } + + await client.disconnect() + + except Exception: + import logging + + logging.getLogger(__name__).warning( + "Failed to load tool server '%s'", server_id, exc_info=True + ) + + _tool_server_cache = {"servers": servers, "tools": tools} + return _tool_server_cache + + +def invalidate_tool_server_cache() -> None: + """Clear the external tool server cache, forcing a reload on next access.""" + global _tool_server_cache + _tool_server_cache = None + + +def _build_server_headers(server: dict) -> dict | None: + """Build auth + custom headers for a tool server connection.""" + headers = dict(server.get("headers") or {}) + auth_type = server.get("auth_type", "bearer") + if auth_type == "bearer": + key = server.get("key", "") + if key: + headers["Authorization"] = f"Bearer {key}" + return headers or None + + +async def _execute_external_tool(name: str, args: dict) -> str: + """Execute an external tool by its prefixed name ({server_id}_{tool_name}).""" + cache = await _load_tool_servers() + tool_info = cache["tools"].get(name) + if not tool_info: + return f"Error: external tool '{name}' not found" + + server = tool_info["server"] + original_name = tool_info["original_name"] + tool_type = tool_info["type"] + headers = _build_server_headers(server) + + try: + if tool_type == "mcp": + from cptr.utils.mcp.client import MCPClient + + client = MCPClient() + await client.connect(server.get("url", ""), headers) + try: + result = await client.call_tool(original_name, args) + # MCP returns a list of content items; extract text + texts = [] + for item in result: + if isinstance(item, dict): + if item.get("type") == "text": + texts.append(item.get("text", "")) + else: + texts.append(json.dumps(item)) + return "\n".join(texts) if texts else "(no output)" + finally: + await client.disconnect() + + elif tool_type == "openapi": + from cptr.utils.openapi import execute_openapi_tool + + openapi_spec = server.get("_openapi_spec", {}) + return await execute_openapi_tool( + server_url=server.get("url", "").rstrip("/"), + openapi_spec=openapi_spec, + tool_name=original_name, + args=args, + headers=headers, + ) + + else: + return f"Error: unknown tool server type: {tool_type}" + + except Exception as e: + return f"Error executing external tool '{name}': {e}" + + # ── Schema from function signature ────────────────────────── _TYPE_MAP = {str: "string", int: "integer", bool: "boolean", float: "number"} @@ -1264,7 +1556,8 @@ def _fn_to_schema(name: str, fn) -> dict: async def get_tool_list() -> list[dict]: """Return tool schemas for the LLM. - Automatically includes browser tools when browser.enabled is true in config. + Automatically includes browser tools when browser.enabled is true, + and external tool server tools when configured. """ tools = dict(TOOLS) try: @@ -1272,23 +1565,42 @@ async def get_tool_list() -> list[dict]: if (await Config.get("browser.enabled")) in (True, "true", "1"): tools.update(BROWSER_TOOLS) + if (await Config.get("subagents.enabled")) in (True, "true", "1"): + tools.update(SUBAGENT_TOOLS) + except Exception: + pass + + schemas = [_fn_to_schema(name, t["fn"]) for name, t in tools.items()] + + # Add external tool server schemas + try: + cache = await _load_tool_servers() + for tool_info in cache["tools"].values(): + schemas.append(tool_info["spec"]) except Exception: pass - return [_fn_to_schema(name, t["fn"]) for name, t in tools.items()] + + return schemas async def execute_tool(name: str, args: dict, __context__: dict) -> str: """Execute a tool by name, injecting execution context.""" - info = TOOLS.get(name) or BROWSER_TOOLS.get(name) - if not info: - return f"Error: unknown tool: {name}" - fn = info["fn"] - try: - sig = inspect.signature(fn) - if "__context__" in sig.parameters: - return await fn(**args, __context__=__context__) - else: - # Legacy tools: inject workspace directly - return await fn(**args, workspace=__context__["workspace"]) - except Exception as e: - return f"Error executing {name}: {e}" + info = ALL_TOOLS.get(name) + if info: + fn = info["fn"] + try: + sig = inspect.signature(fn) + if "__context__" in sig.parameters: + return await fn(**args, __context__=__context__) + else: + # Legacy tools: inject workspace directly + return await fn(**args, workspace=__context__["workspace"]) + except Exception as e: + return f"Error executing {name}: {e}" + + # Check external tool servers + cache = await _load_tool_servers() + if name in cache["tools"]: + return await _execute_external_tool(name, args) + + return f"Error: unknown tool: {name}" diff --git a/pyproject.toml b/pyproject.toml index bda26ce..786967b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cptr" -version = "0.3.5" +version = "0.4.0" description = "Your computer, from anywhere. Code, manage, and control your machine from the web." license = {file = "LICENSE"} readme = "README.md" @@ -24,6 +24,7 @@ dependencies = [ [project.optional-dependencies] pam = ["python-pam>=2.0"] +mcp = ["mcp>=1.8"] [dependency-groups] dev = [