diff --git a/.github/workflows/auto-i18n.yml b/.github/workflows/auto-i18n.yml index 2ca56c0837..7537c4d4a3 100644 --- a/.github/workflows/auto-i18n.yml +++ b/.github/workflows/auto-i18n.yml @@ -73,7 +73,7 @@ jobs: - name: 🚀 Create Pull Request if changes exist if: steps.git_status.outputs.has_changes == 'true' - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions commit-message: "feat(bot): Weekly automated script run" diff --git a/README.md b/README.md index f790c10cbd..781e9299e5 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ -

English | 中文 | Official Site | Documents | Development | Feedback

+

English | 中文 | Official Site | Documents | Development | Feedback

@@ -242,12 +242,12 @@ The Enterprise Edition addresses core challenges in team collaboration by centra ## Version Comparison -| Feature | Community Edition | Enterprise Edition | -| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | -| **Open Source** | ✅ Yes | ⭕️ Partially released to customers | +| Feature | Community Edition | Enterprise Edition | +| :---------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | +| **Open Source** | ✅ Yes | ⭕️ Partially released to customers | | **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee | -| **Admin Backend** | — | ● Centralized **Model** Access
● **Employee** Management
● Shared **Knowledge Base**
● **Access** Control
● **Data** Backup | -| **Server** | — | ✅ Dedicated Private Deployment | +| **Admin Backend** | — | ● Centralized **Model** Access
● **Employee** Management
● Shared **Knowledge Base**
● **Access** Control
● **Data** Backup | +| **Server** | — | ✅ Dedicated Private Deployment | ## Get the Enterprise Edition @@ -275,7 +275,7 @@ We believe the Enterprise Edition will become your team's AI productivity engine # 📊 GitHub Stats -![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image') +![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg "Repobeats analytics image") # ⭐️ Star History diff --git a/docs/zh/README.md b/docs/zh/README.md index f8a1f1ab8c..c4adeb4901 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -34,7 +34,7 @@

- English | 中文 | 官方网站 | 文档 | 开发 | 反馈
+ English | 中文 | 官方网站 | 文档 | 开发 | 反馈

@@ -281,7 +281,7 @@ https://docs.cherry-ai.com # 📊 GitHub 统计 -![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image') +![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg "Repobeats analytics image") # ⭐️ Star 记录 diff --git a/scripts/auto-translate-i18n.ts b/scripts/auto-translate-i18n.ts index f57913b014..41bb14a0a1 100644 --- a/scripts/auto-translate-i18n.ts +++ b/scripts/auto-translate-i18n.ts @@ -152,7 +152,8 @@ const languageMap = { 'es-es': 'Spanish', 'fr-fr': 'French', 'pt-pt': 'Portuguese', - 'de-de': 'German' + 'de-de': 'German', + 'ro-ro': 'Romanian' } const PROMPT = ` diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 56932a51d6..ccaa664ab8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1061,12 +1061,18 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) } catch (error) { const pluginError = extractPluginError(error) if (pluginError) { - logger.error('Failed to list installed plugins', { agentId, error: pluginError }) + logger.error('Failed to list installed plugins', { + agentId, + error: pluginError + }) return { success: false, error: pluginError } } const err = normalizeError(error) - logger.error('Failed to list installed plugins', { agentId, error: err }) + logger.error('Failed to list installed plugins', { + agentId, + error: err + }) return { success: false, error: { diff --git a/src/main/mcpServers/__tests__/browser.test.ts b/src/main/mcpServers/__tests__/browser.test.ts index 712eaf94ea..800d03d7c5 100644 --- a/src/main/mcpServers/__tests__/browser.test.ts +++ b/src/main/mcpServers/__tests__/browser.test.ts @@ -1,5 +1,14 @@ import { describe, expect, it, vi } from 'vitest' +vi.mock('node:fs', () => ({ + default: { + existsSync: vi.fn(() => false), + mkdirSync: vi.fn() + }, + existsSync: vi.fn(() => false), + mkdirSync: vi.fn() +})) + vi.mock('electron', () => { const sendCommand = vi.fn(async (command: string, params?: { expression?: string }) => { if (command === 'Runtime.evaluate') { @@ -21,24 +30,31 @@ vi.mock('electron', () => { sendCommand } - const webContents = { + const createWebContents = () => ({ debugger: debuggerObj, setUserAgent: vi.fn(), getURL: vi.fn(() => 'https://example.com/'), getTitle: vi.fn(async () => 'Example Title'), + loadURL: vi.fn(async () => {}), once: vi.fn(), removeListener: vi.fn(), - on: vi.fn() - } - - const loadURL = vi.fn(async () => {}) + on: vi.fn(), + isDestroyed: vi.fn(() => false), + canGoBack: vi.fn(() => false), + canGoForward: vi.fn(() => false), + goBack: vi.fn(), + goForward: vi.fn(), + reload: vi.fn(), + executeJavaScript: vi.fn(async () => null), + setWindowOpenHandler: vi.fn() + }) const windows: any[] = [] + const views: any[] = [] class MockBrowserWindow { private destroyed = false - public webContents = webContents - public loadURL = loadURL + public webContents = createWebContents() public isDestroyed = vi.fn(() => this.destroyed) public close = vi.fn(() => { this.destroyed = true @@ -47,31 +63,58 @@ vi.mock('electron', () => { this.destroyed = true }) public on = vi.fn() + public setBrowserView = vi.fn() + public addBrowserView = vi.fn() + public removeBrowserView = vi.fn() + public getContentSize = vi.fn(() => [1200, 800]) + public show = vi.fn() constructor() { windows.push(this) } } + class MockBrowserView { + public webContents = createWebContents() + public setBounds = vi.fn() + public setAutoResize = vi.fn() + public destroy = vi.fn() + + constructor() { + views.push(this) + } + } + const app = { isReady: vi.fn(() => true), whenReady: vi.fn(async () => {}), - on: vi.fn() + on: vi.fn(), + getPath: vi.fn((key: string) => { + if (key === 'userData') return '/mock/userData' + if (key === 'temp') return '/tmp' + return '/mock/unknown' + }), + getAppPath: vi.fn(() => '/mock/app'), + setPath: vi.fn() + } + + const nativeTheme = { + on: vi.fn(), + shouldUseDarkColors: false } return { BrowserWindow: MockBrowserWindow as any, + BrowserView: MockBrowserView as any, app, + nativeTheme, __mockDebugger: debuggerObj, __mockSendCommand: sendCommand, - __mockLoadURL: loadURL, - __mockWindows: windows + __mockWindows: windows, + __mockViews: views } }) -import * as electron from 'electron' -const { __mockWindows } = electron as typeof electron & { __mockWindows: any[] } - import { CdpBrowserController } from '../browser' describe('CdpBrowserController', () => { @@ -81,54 +124,249 @@ describe('CdpBrowserController', () => { expect(result).toBe('ok') }) - it('opens a URL (hidden) and returns current page info', async () => { + it('opens a URL in normal mode and returns current page info', async () => { const controller = new CdpBrowserController() const result = await controller.open('https://foo.bar/', 5000, false) expect(result.currentUrl).toBe('https://example.com/') expect(result.title).toBe('Example Title') }) - it('opens a URL (visible) when show=true', async () => { + it('opens a URL in private mode', async () => { const controller = new CdpBrowserController() - const result = await controller.open('https://foo.bar/', 5000, true, 'session-a') + const result = await controller.open('https://foo.bar/', 5000, true) expect(result.currentUrl).toBe('https://example.com/') expect(result.title).toBe('Example Title') }) it('reuses session for execute and supports multiline', async () => { const controller = new CdpBrowserController() - await controller.open('https://foo.bar/', 5000, false, 'session-b') - const result = await controller.execute('const a=1; const b=2; a+b;', 5000, 'session-b') + await controller.open('https://foo.bar/', 5000, false) + const result = await controller.execute('const a=1; const b=2; a+b;', 5000, false) expect(result).toBe('ok') }) - it('evicts least recently used session when exceeding maxSessions', async () => { - const controller = new CdpBrowserController({ maxSessions: 2, idleTimeoutMs: 1000 * 60 }) - await controller.open('https://foo.bar/', 5000, false, 's1') - await controller.open('https://foo.bar/', 5000, false, 's2') - await controller.open('https://foo.bar/', 5000, false, 's3') - const destroyedCount = __mockWindows.filter( - (w: any) => w.destroy.mock.calls.length > 0 || w.close.mock.calls.length > 0 - ).length - expect(destroyedCount).toBeGreaterThanOrEqual(1) + it('normal and private modes are isolated', async () => { + const controller = new CdpBrowserController() + await controller.open('https://foo.bar/', 5000, false) + await controller.open('https://foo.bar/', 5000, true) + const normalResult = await controller.execute('1+1', 5000, false) + const privateResult = await controller.execute('1+1', 5000, true) + expect(normalResult).toBe('ok') + expect(privateResult).toBe('ok') }) - it('fetches URL and returns html format', async () => { + it('fetches URL and returns html format with tabId', async () => { const controller = new CdpBrowserController() const result = await controller.fetch('https://example.com/', 'html') - expect(result).toBe('

Test

Content

') + expect(result.tabId).toBeDefined() + expect(result.content).toBe('

Test

Content

') }) - it('fetches URL and returns txt format', async () => { + it('fetches URL and returns txt format with tabId', async () => { const controller = new CdpBrowserController() const result = await controller.fetch('https://example.com/', 'txt') - expect(result).toBe('Test\nContent') + expect(result.tabId).toBeDefined() + expect(result.content).toBe('Test\nContent') }) - it('fetches URL and returns markdown format (default)', async () => { + it('fetches URL and returns markdown format (default) with tabId', async () => { const controller = new CdpBrowserController() const result = await controller.fetch('https://example.com/') - expect(typeof result).toBe('string') - expect(result).toContain('Test') + expect(result.tabId).toBeDefined() + expect(typeof result.content).toBe('string') + expect(result.content).toContain('Test') + }) + + it('fetches URL in private mode with tabId', async () => { + const controller = new CdpBrowserController() + const result = await controller.fetch('https://example.com/', 'html', 10000, true) + expect(result.tabId).toBeDefined() + expect(result.content).toBe('

Test

Content

') + }) + + describe('Multi-tab support', () => { + it('creates new tab with newTab parameter', async () => { + const controller = new CdpBrowserController() + const result1 = await controller.open('https://site1.com/', 5000, false, true) + const result2 = await controller.open('https://site2.com/', 5000, false, true) + + expect(result1.tabId).toBeDefined() + expect(result2.tabId).toBeDefined() + expect(result1.tabId).not.toBe(result2.tabId) + }) + + it('reuses same tab without newTab parameter', async () => { + const controller = new CdpBrowserController() + const result1 = await controller.open('https://site1.com/', 5000, false) + const result2 = await controller.open('https://site2.com/', 5000, false) + + expect(result1.tabId).toBe(result2.tabId) + }) + + it('fetches in new tab with newTab parameter', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + const tabs = await controller.listTabs(false) + const initialTabCount = tabs.length + + await controller.fetch('https://other.com/', 'html', 10000, false, true) + const tabsAfter = await controller.listTabs(false) + + expect(tabsAfter.length).toBe(initialTabCount + 1) + }) + }) + + describe('Tab management', () => { + it('lists tabs in a window', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + + const tabs = await controller.listTabs(false) + expect(tabs.length).toBeGreaterThan(0) + expect(tabs[0].tabId).toBeDefined() + }) + + it('lists tabs separately for normal and private modes', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + await controller.open('https://example.com/', 5000, true) + + const normalTabs = await controller.listTabs(false) + const privateTabs = await controller.listTabs(true) + + expect(normalTabs.length).toBe(1) + expect(privateTabs.length).toBe(1) + expect(normalTabs[0].tabId).not.toBe(privateTabs[0].tabId) + }) + + it('closes specific tab', async () => { + const controller = new CdpBrowserController() + const result1 = await controller.open('https://site1.com/', 5000, false, true) + await controller.open('https://site2.com/', 5000, false, true) + + const tabsBefore = await controller.listTabs(false) + expect(tabsBefore.length).toBe(2) + + await controller.closeTab(false, result1.tabId) + + const tabsAfter = await controller.listTabs(false) + expect(tabsAfter.length).toBe(1) + expect(tabsAfter.find((t) => t.tabId === result1.tabId)).toBeUndefined() + }) + + it('switches active tab', async () => { + const controller = new CdpBrowserController() + const result1 = await controller.open('https://site1.com/', 5000, false, true) + const result2 = await controller.open('https://site2.com/', 5000, false, true) + + await controller.switchTab(false, result1.tabId) + await controller.switchTab(false, result2.tabId) + }) + + it('throws error when switching to non-existent tab', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + + await expect(controller.switchTab(false, 'non-existent-tab')).rejects.toThrow('Tab non-existent-tab not found') + }) + }) + + describe('Reset behavior', () => { + it('resets specific tab only', async () => { + const controller = new CdpBrowserController() + const result1 = await controller.open('https://site1.com/', 5000, false, true) + await controller.open('https://site2.com/', 5000, false, true) + + await controller.reset(false, result1.tabId) + + const tabs = await controller.listTabs(false) + expect(tabs.length).toBe(1) + }) + + it('resets specific window only', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + await controller.open('https://example.com/', 5000, true) + + await controller.reset(false) + + const normalTabs = await controller.listTabs(false) + const privateTabs = await controller.listTabs(true) + + expect(normalTabs.length).toBe(0) + expect(privateTabs.length).toBe(1) + }) + + it('resets all windows', async () => { + const controller = new CdpBrowserController() + await controller.open('https://example.com/', 5000, false) + await controller.open('https://example.com/', 5000, true) + + await controller.reset() + + const normalTabs = await controller.listTabs(false) + const privateTabs = await controller.listTabs(true) + + expect(normalTabs.length).toBe(0) + expect(privateTabs.length).toBe(0) + }) + }) + + describe('showWindow parameter', () => { + it('passes showWindow parameter through open', async () => { + const controller = new CdpBrowserController() + const result = await controller.open('https://example.com/', 5000, false, false, true) + expect(result.currentUrl).toBe('https://example.com/') + expect(result.tabId).toBeDefined() + }) + + it('passes showWindow parameter through fetch', async () => { + const controller = new CdpBrowserController() + const result = await controller.fetch('https://example.com/', 'html', 10000, false, false, true) + expect(result.tabId).toBeDefined() + expect(result.content).toBe('

Test

Content

') + }) + + it('passes showWindow parameter through createTab', async () => { + const controller = new CdpBrowserController() + const { tabId, view } = await controller.createTab(false, true) + expect(tabId).toBeDefined() + expect(view).toBeDefined() + }) + + it('shows existing window when showWindow=true on subsequent calls', async () => { + const controller = new CdpBrowserController() + // First call creates window + await controller.open('https://example.com/', 5000, false, false, false) + // Second call with showWindow=true should show existing window + const result = await controller.open('https://example.com/', 5000, false, false, true) + expect(result.currentUrl).toBe('https://example.com/') + }) + }) + + describe('Window limits and eviction', () => { + it('respects maxWindows limit', async () => { + const controller = new CdpBrowserController({ maxWindows: 1 }) + await controller.open('https://example.com/', 5000, false) + await controller.open('https://example.com/', 5000, true) + + const normalTabs = await controller.listTabs(false) + const privateTabs = await controller.listTabs(true) + + expect(privateTabs.length).toBe(1) + expect(normalTabs.length).toBe(0) + }) + + it('cleans up idle windows on next access', async () => { + const controller = new CdpBrowserController({ idleTimeoutMs: 1 }) + await controller.open('https://example.com/', 5000, false) + + await new Promise((r) => setTimeout(r, 10)) + + await controller.open('https://example.com/', 5000, true) + + const normalTabs = await controller.listTabs(false) + expect(normalTabs.length).toBe(0) + }) }) }) diff --git a/src/main/mcpServers/browser/README.md b/src/main/mcpServers/browser/README.md new file mode 100644 index 0000000000..27d1307782 --- /dev/null +++ b/src/main/mcpServers/browser/README.md @@ -0,0 +1,177 @@ +# Browser MCP Server + +A Model Context Protocol (MCP) server for controlling browser windows via Chrome DevTools Protocol (CDP). + +## Features + +### ✨ User Data Persistence +- **Normal mode (default)**: Cookies, localStorage, and sessionStorage persist across browser restarts +- **Private mode**: Ephemeral browsing - no data persists (like incognito mode) + +### 🔄 Window Management +- Two browsing modes: normal (persistent) and private (ephemeral) +- Lazy idle timeout cleanup (cleaned on next window access) +- Maximum window limits to prevent resource exhaustion + +> **Note**: Normal mode uses a global `persist:default` partition shared by all clients. This means login sessions and stored data are accessible to any code using the MCP server. + +## Architecture + +### How It Works +``` +Normal Mode (BrowserWindow) +├─ Persistent Storage (partition: persist:default) ← Global, shared across all clients +└─ Tabs (BrowserView) ← created via newTab or automatically + +Private Mode (BrowserWindow) +├─ Ephemeral Storage (partition: private) ← No disk persistence +└─ Tabs (BrowserView) ← created via newTab or automatically +``` + +- **One Window Per Mode**: Normal and private modes each have their own window +- **Multi-Tab Support**: Use `newTab: true` for parallel URL requests +- **Storage Isolation**: Normal and private modes have completely separate storage + +## Available Tools + +### `open` +Open a URL in a browser window. Optionally return page content. +```json +{ + "url": "https://example.com", + "format": "markdown", + "timeout": 10000, + "privateMode": false, + "newTab": false, + "showWindow": false +} +``` +- `format`: If set (`html`, `txt`, `markdown`, `json`), returns page content in that format along with tabId. If not set, just opens the page and returns navigation info. +- `newTab`: Set to `true` to open in a new tab (required for parallel requests) +- `showWindow`: Set to `true` to display the browser window (useful for debugging) +- Returns (without format): `{ currentUrl, title, tabId }` +- Returns (with format): `{ tabId, content }` where content is in the specified format + +### `execute` +Execute JavaScript code in the page context. +```json +{ + "code": "document.title", + "timeout": 5000, + "privateMode": false, + "tabId": "optional-tab-id" +} +``` +- `tabId`: Target a specific tab (from `open` response) + +### `reset` +Reset browser windows and tabs. +```json +{ + "privateMode": false, + "tabId": "optional-tab-id" +} +``` +- Omit all parameters to close all windows +- Set `privateMode` to close a specific window +- Set both `privateMode` and `tabId` to close a specific tab only + +## Usage Examples + +### Basic Navigation +```typescript +// Open a URL in normal mode (data persists) +await controller.open('https://example.com') +``` + +### Fetch Page Content +```typescript +// Open URL and get content as markdown +await open({ url: 'https://example.com', format: 'markdown' }) + +// Open URL and get raw HTML +await open({ url: 'https://example.com', format: 'html' }) +``` + +### Multi-Tab / Parallel Requests +```typescript +// Open multiple URLs in parallel using newTab +const [page1, page2] = await Promise.all([ + controller.open('https://site1.com', 10000, false, true), // newTab: true + controller.open('https://site2.com', 10000, false, true) // newTab: true +]) + +// Execute on specific tab +await controller.execute('document.title', 5000, false, page1.tabId) + +// Close specific tab when done +await controller.reset(false, page1.tabId) +``` + +### Private Browsing +```typescript +// Open a URL in private mode (no data persistence) +await controller.open('https://example.com', 10000, true) + +// Cookies and localStorage won't persist after reset +``` + +### Data Persistence (Normal Mode) +```typescript +// Set data +await controller.open('https://example.com', 10000, false) +await controller.execute('localStorage.setItem("key", "value")', 5000, false) + +// Close window +await controller.reset(false) + +// Reopen - data persists! +await controller.open('https://example.com', 10000, false) +const value = await controller.execute('localStorage.getItem("key")', 5000, false) +// Returns: "value" +``` + +### No Persistence (Private Mode) +```typescript +// Set data in private mode +await controller.open('https://example.com', 10000, true) +await controller.execute('localStorage.setItem("key", "value")', 5000, true) + +// Close private window +await controller.reset(true) + +// Reopen - data is gone! +await controller.open('https://example.com', 10000, true) +const value = await controller.execute('localStorage.getItem("key")', 5000, true) +// Returns: null +``` + +## Configuration + +```typescript +const controller = new CdpBrowserController({ + maxWindows: 5, // Maximum concurrent windows + idleTimeoutMs: 5 * 60 * 1000 // 5 minutes idle timeout (lazy cleanup) +}) +``` + +> **Note on Idle Timeout**: Idle windows are cleaned up lazily when the next window is created or accessed, not on a background timer. + +## Best Practices + +1. **Use Normal Mode for Authentication**: When you need to stay logged in across sessions +2. **Use Private Mode for Sensitive Operations**: When you don't want data to persist +3. **Use `newTab: true` for Parallel Requests**: Avoid race conditions when fetching multiple URLs +4. **Resource Cleanup**: Call `reset()` when done, or `reset(privateMode, tabId)` to close specific tabs +5. **Error Handling**: All tool handlers return error responses on failure +6. **Timeout Configuration**: Adjust timeouts based on page complexity + +## Technical Details + +- **CDP Version**: 1.3 +- **User Agent**: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +- **Storage**: + - Normal mode: `persist:default` (disk-persisted, global) + - Private mode: `private` (memory only) +- **Window Size**: 1200x800 (default) +- **Visibility**: Windows hidden by default (use `showWindow: true` to display) diff --git a/src/main/mcpServers/browser/constants.ts b/src/main/mcpServers/browser/constants.ts new file mode 100644 index 0000000000..2b10943f8e --- /dev/null +++ b/src/main/mcpServers/browser/constants.ts @@ -0,0 +1,3 @@ +export const TAB_BAR_HEIGHT = 92 // Height for Chrome-style tab bar (42px) + address bar (50px) +export const SESSION_KEY_DEFAULT = 'default' +export const SESSION_KEY_PRIVATE = 'private' diff --git a/src/main/mcpServers/browser/controller.ts b/src/main/mcpServers/browser/controller.ts index 6246da45d2..9e0f5220ca 100644 --- a/src/main/mcpServers/browser/controller.ts +++ b/src/main/mcpServers/browser/controller.ts @@ -1,20 +1,49 @@ -import { app, BrowserWindow } from 'electron' +import { titleBarOverlayDark, titleBarOverlayLight } from '@main/config' +import { isMac } from '@main/constant' +import { randomUUID } from 'crypto' +import { app, BrowserView, BrowserWindow, nativeTheme } from 'electron' import TurndownService from 'turndown' -import { logger, userAgent } from './types' +import { SESSION_KEY_DEFAULT, SESSION_KEY_PRIVATE, TAB_BAR_HEIGHT } from './constants' +import { TAB_BAR_HTML } from './tabbar-html' +import { logger, type TabInfo, userAgent, type WindowInfo } from './types' /** * Controller for managing browser windows via Chrome DevTools Protocol (CDP). - * Supports multiple sessions with LRU eviction and idle timeout cleanup. + * Supports two modes: normal (persistent) and private (ephemeral). + * Normal mode persists user data (cookies, localStorage, etc.) globally across all clients. + * Private mode is ephemeral - data is cleared when the window closes. */ export class CdpBrowserController { - private windows: Map = new Map() - private readonly maxSessions: number + private windows: Map = new Map() + private readonly maxWindows: number private readonly idleTimeoutMs: number + private readonly turndownService: TurndownService - constructor(options?: { maxSessions?: number; idleTimeoutMs?: number }) { - this.maxSessions = options?.maxSessions ?? 5 + constructor(options?: { maxWindows?: number; idleTimeoutMs?: number }) { + this.maxWindows = options?.maxWindows ?? 5 this.idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000 + this.turndownService = new TurndownService() + + // Listen for theme changes and update all tab bars + nativeTheme.on('updated', () => { + const isDark = nativeTheme.shouldUseDarkColors + for (const windowInfo of this.windows.values()) { + if (windowInfo.tabBarView && !windowInfo.tabBarView.webContents.isDestroyed()) { + windowInfo.tabBarView.webContents.executeJavaScript(`window.setTheme(${isDark})`).catch(() => { + // Ignore errors if tab bar is not ready + }) + } + } + }) + } + + private getWindowKey(privateMode: boolean): string { + return privateMode ? SESSION_KEY_PRIVATE : SESSION_KEY_DEFAULT + } + + private getPartition(privateMode: boolean): string { + return privateMode ? SESSION_KEY_PRIVATE : `persist:${SESSION_KEY_DEFAULT}` } private async ensureAppReady() { @@ -23,28 +52,50 @@ export class CdpBrowserController { } } - private touch(sessionId: string) { - const entry = this.windows.get(sessionId) - if (entry) entry.lastActive = Date.now() + private touchWindow(windowKey: string) { + const windowInfo = this.windows.get(windowKey) + if (windowInfo) windowInfo.lastActive = Date.now() } - private closeWindow(win: BrowserWindow, sessionId: string) { - try { - if (!win.isDestroyed()) { - if (win.webContents.debugger.isAttached()) { - win.webContents.debugger.detach() - } - win.close() - } - } catch (error) { - logger.warn('Error closing window', { error, sessionId }) + private touchTab(windowKey: string, tabId: string) { + const windowInfo = this.windows.get(windowKey) + if (windowInfo) { + const tab = windowInfo.tabs.get(tabId) + if (tab) tab.lastActive = Date.now() + windowInfo.lastActive = Date.now() } } - private async ensureDebuggerAttached(dbg: Electron.Debugger, sessionId: string) { + private closeTabInternal(windowInfo: WindowInfo, tabId: string) { + try { + const tab = windowInfo.tabs.get(tabId) + if (!tab) return + + if (!tab.view.webContents.isDestroyed()) { + if (tab.view.webContents.debugger.isAttached()) { + tab.view.webContents.debugger.detach() + } + } + + // Remove view from window + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.removeBrowserView(tab.view) + } + + // Destroy the view using safe cast + const viewWithDestroy = tab.view as BrowserView & { destroy?: () => void } + if (viewWithDestroy.destroy) { + viewWithDestroy.destroy() + } + } catch (error) { + logger.warn('Error closing tab', { error, windowKey: windowInfo.windowKey, tabId }) + } + } + + private async ensureDebuggerAttached(dbg: Electron.Debugger, sessionKey: string) { if (!dbg.isAttached()) { try { - logger.info('Attaching debugger', { sessionId }) + logger.info('Attaching debugger', { sessionKey }) dbg.attach('1.3') await dbg.sendCommand('Page.enable') await dbg.sendCommand('Runtime.enable') @@ -58,110 +109,514 @@ export class CdpBrowserController { private sweepIdle() { const now = Date.now() - for (const [id, entry] of this.windows.entries()) { - if (now - entry.lastActive > this.idleTimeoutMs) { - this.closeWindow(entry.win, id) - this.windows.delete(id) + const windowKeys = Array.from(this.windows.keys()) + for (const windowKey of windowKeys) { + const windowInfo = this.windows.get(windowKey) + if (!windowInfo) continue + if (now - windowInfo.lastActive > this.idleTimeoutMs) { + const tabIds = Array.from(windowInfo.tabs.keys()) + for (const tabId of tabIds) { + this.closeTabInternal(windowInfo, tabId) + } + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } + this.windows.delete(windowKey) } } } - private evictIfNeeded(newSessionId: string) { - if (this.windows.size < this.maxSessions) return - let lruId: string | null = null + private evictIfNeeded(newWindowKey: string) { + if (this.windows.size < this.maxWindows) return + let lruKey: string | null = null let lruTime = Number.POSITIVE_INFINITY - for (const [id, entry] of this.windows.entries()) { - if (id === newSessionId) continue - if (entry.lastActive < lruTime) { - lruTime = entry.lastActive - lruId = id + for (const [key, windowInfo] of this.windows.entries()) { + if (key === newWindowKey) continue + if (windowInfo.lastActive < lruTime) { + lruTime = windowInfo.lastActive + lruKey = key } } - if (lruId) { - const entry = this.windows.get(lruId) - if (entry) { - this.closeWindow(entry.win, lruId) + if (lruKey) { + const windowInfo = this.windows.get(lruKey) + if (windowInfo) { + for (const [tabId] of windowInfo.tabs.entries()) { + this.closeTabInternal(windowInfo, tabId) + } + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } } - this.windows.delete(lruId) - logger.info('Evicted session to respect maxSessions', { evicted: lruId }) + this.windows.delete(lruKey) + logger.info('Evicted window to respect maxWindows', { evicted: lruKey }) } } - private async getWindow(sessionId = 'default', forceNew = false, show = false): Promise { + private sendTabBarUpdate(windowInfo: WindowInfo) { + if (!windowInfo.tabBarView || !windowInfo.tabBarView.webContents || windowInfo.tabBarView.webContents.isDestroyed()) + return + + const tabs = Array.from(windowInfo.tabs.values()).map((tab) => ({ + id: tab.id, + title: tab.title || 'New Tab', + url: tab.url, + isActive: tab.id === windowInfo.activeTabId + })) + + let activeUrl = '' + let canGoBack = false + let canGoForward = false + + if (windowInfo.activeTabId) { + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (activeTab && !activeTab.view.webContents.isDestroyed()) { + activeUrl = activeTab.view.webContents.getURL() + canGoBack = activeTab.view.webContents.canGoBack() + canGoForward = activeTab.view.webContents.canGoForward() + } + } + + const script = `window.updateTabs(${JSON.stringify(tabs)}, ${JSON.stringify(activeUrl)}, ${canGoBack}, ${canGoForward})` + windowInfo.tabBarView.webContents.executeJavaScript(script).catch((error) => { + logger.debug('Tab bar update failed', { error, windowKey: windowInfo.windowKey }) + }) + } + + private handleNavigateAction(windowInfo: WindowInfo, url: string) { + if (!windowInfo.activeTabId) return + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (!activeTab || activeTab.view.webContents.isDestroyed()) return + + let finalUrl = url.trim() + if (!/^https?:\/\//i.test(finalUrl)) { + if (/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/.test(finalUrl) || finalUrl.includes('.')) { + finalUrl = 'https://' + finalUrl + } else { + finalUrl = 'https://www.google.com/search?q=' + encodeURIComponent(finalUrl) + } + } + + activeTab.view.webContents.loadURL(finalUrl).catch((error) => { + logger.warn('Navigation failed in tab bar', { error, url: finalUrl, tabId: windowInfo.activeTabId }) + }) + } + + private handleBackAction(windowInfo: WindowInfo) { + if (!windowInfo.activeTabId) return + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (!activeTab || activeTab.view.webContents.isDestroyed()) return + + if (activeTab.view.webContents.canGoBack()) { + activeTab.view.webContents.goBack() + } + } + + private handleForwardAction(windowInfo: WindowInfo) { + if (!windowInfo.activeTabId) return + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (!activeTab || activeTab.view.webContents.isDestroyed()) return + + if (activeTab.view.webContents.canGoForward()) { + activeTab.view.webContents.goForward() + } + } + + private handleRefreshAction(windowInfo: WindowInfo) { + if (!windowInfo.activeTabId) return + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (!activeTab || activeTab.view.webContents.isDestroyed()) return + + activeTab.view.webContents.reload() + } + + private setupTabBarMessageHandler(windowInfo: WindowInfo) { + if (!windowInfo.tabBarView) return + + windowInfo.tabBarView.webContents.on('console-message', (_event, _level, message) => { + try { + const parsed = JSON.parse(message) + if (parsed?.channel === 'tabbar-action' && parsed?.payload) { + this.handleTabBarAction(windowInfo, parsed.payload) + } + } catch { + // Not a JSON message, ignore + } + }) + + windowInfo.tabBarView.webContents + .executeJavaScript(` + (function() { + window.addEventListener('message', function(e) { + if (e.data && e.data.channel === 'tabbar-action') { + console.log(JSON.stringify(e.data)); + } + }); + })(); + `) + .catch((error) => { + logger.debug('Tab bar message handler setup failed', { error, windowKey: windowInfo.windowKey }) + }) + } + + private handleTabBarAction(windowInfo: WindowInfo, action: { type: string; tabId?: string; url?: string }) { + if (action.type === 'switch' && action.tabId) { + this.switchTab(windowInfo.privateMode, action.tabId).catch((error) => { + logger.warn('Tab switch failed', { error, tabId: action.tabId, windowKey: windowInfo.windowKey }) + }) + } else if (action.type === 'close' && action.tabId) { + this.closeTab(windowInfo.privateMode, action.tabId).catch((error) => { + logger.warn('Tab close failed', { error, tabId: action.tabId, windowKey: windowInfo.windowKey }) + }) + } else if (action.type === 'new') { + this.createTab(windowInfo.privateMode, true) + .then(({ tabId }) => this.switchTab(windowInfo.privateMode, tabId)) + .catch((error) => { + logger.warn('New tab creation failed', { error, windowKey: windowInfo.windowKey }) + }) + } else if (action.type === 'navigate' && action.url) { + this.handleNavigateAction(windowInfo, action.url) + } else if (action.type === 'back') { + this.handleBackAction(windowInfo) + } else if (action.type === 'forward') { + this.handleForwardAction(windowInfo) + } else if (action.type === 'refresh') { + this.handleRefreshAction(windowInfo) + } else if (action.type === 'window-minimize') { + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.minimize() + } + } else if (action.type === 'window-maximize') { + if (!windowInfo.window.isDestroyed()) { + if (windowInfo.window.isMaximized()) { + windowInfo.window.unmaximize() + } else { + windowInfo.window.maximize() + } + } + } else if (action.type === 'window-close') { + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } + } + } + + private createTabBarView(windowInfo: WindowInfo): BrowserView { + const tabBarView = new BrowserView({ + webPreferences: { + contextIsolation: false, + sandbox: false, + nodeIntegration: false + } + }) + + windowInfo.window.addBrowserView(tabBarView) + const [width] = windowInfo.window.getContentSize() + tabBarView.setBounds({ x: 0, y: 0, width, height: TAB_BAR_HEIGHT }) + tabBarView.setAutoResize({ width: true, height: false }) + tabBarView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(TAB_BAR_HTML)}`) + + tabBarView.webContents.on('did-finish-load', () => { + // Initialize platform for proper styling + const platform = isMac ? 'mac' : process.platform === 'win32' ? 'win' : 'linux' + tabBarView.webContents.executeJavaScript(`window.initPlatform('${platform}')`).catch((error) => { + logger.debug('Platform init failed', { error, windowKey: windowInfo.windowKey }) + }) + // Initialize theme + const isDark = nativeTheme.shouldUseDarkColors + tabBarView.webContents.executeJavaScript(`window.setTheme(${isDark})`).catch((error) => { + logger.debug('Theme init failed', { error, windowKey: windowInfo.windowKey }) + }) + this.setupTabBarMessageHandler(windowInfo) + this.sendTabBarUpdate(windowInfo) + }) + + return tabBarView + } + + private async createBrowserWindow( + windowKey: string, + privateMode: boolean, + showWindow = false + ): Promise { await this.ensureAppReady() - this.sweepIdle() - - const existing = this.windows.get(sessionId) - if (existing && !existing.win.isDestroyed() && !forceNew) { - this.touch(sessionId) - return existing.win - } - - if (existing && !existing.win.isDestroyed() && forceNew) { - try { - if (existing.win.webContents.debugger.isAttached()) { - existing.win.webContents.debugger.detach() - } - } catch (error) { - logger.warn('Error detaching debugger before recreate', { error, sessionId }) - } - existing.win.destroy() - this.windows.delete(sessionId) - } - - this.evictIfNeeded(sessionId) + const partition = this.getPartition(privateMode) const win = new BrowserWindow({ - show, + show: showWindow, + width: 1200, + height: 800, + ...(isMac + ? { + titleBarStyle: 'hidden', + titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, + trafficLightPosition: { x: 8, y: 13 } + } + : { + frame: false // Frameless window for Windows and Linux + }), webPreferences: { contextIsolation: true, sandbox: true, nodeIntegration: false, - devTools: true + devTools: true, + partition } }) - // Use a standard Chrome UA to avoid some anti-bot blocks - win.webContents.setUserAgent(userAgent) - - // Log navigation lifecycle to help diagnose slow loads - win.webContents.on('did-start-loading', () => logger.info(`did-start-loading`, { sessionId })) - win.webContents.on('dom-ready', () => logger.info(`dom-ready`, { sessionId })) - win.webContents.on('did-finish-load', () => logger.info(`did-finish-load`, { sessionId })) - win.webContents.on('did-fail-load', (_e, code, desc) => logger.warn('Navigation failed', { code, desc })) - win.on('closed', () => { - this.windows.delete(sessionId) + const windowInfo = this.windows.get(windowKey) + if (windowInfo) { + const tabIds = Array.from(windowInfo.tabs.keys()) + for (const tabId of tabIds) { + this.closeTabInternal(windowInfo, tabId) + } + this.windows.delete(windowKey) + } }) - this.windows.set(sessionId, { win, lastActive: Date.now() }) return win } + private async getOrCreateWindow(privateMode: boolean, showWindow = false): Promise { + await this.ensureAppReady() + this.sweepIdle() + + const windowKey = this.getWindowKey(privateMode) + + let windowInfo = this.windows.get(windowKey) + if (!windowInfo) { + this.evictIfNeeded(windowKey) + const window = await this.createBrowserWindow(windowKey, privateMode, showWindow) + windowInfo = { + windowKey, + privateMode, + window, + tabs: new Map(), + activeTabId: null, + lastActive: Date.now(), + tabBarView: undefined + } + this.windows.set(windowKey, windowInfo) + const tabBarView = this.createTabBarView(windowInfo) + windowInfo.tabBarView = tabBarView + + // Register resize listener once per window (not per tab) + // Capture windowKey to look up fresh windowInfo on each resize + windowInfo.window.on('resize', () => { + const info = this.windows.get(windowKey) + if (info) this.updateViewBounds(info) + }) + + logger.info('Created new window', { windowKey, privateMode }) + } else if (showWindow && !windowInfo.window.isDestroyed()) { + windowInfo.window.show() + } + + this.touchWindow(windowKey) + return windowInfo + } + + private updateViewBounds(windowInfo: WindowInfo) { + if (windowInfo.window.isDestroyed()) return + + const [width, height] = windowInfo.window.getContentSize() + + // Update tab bar bounds + if (windowInfo.tabBarView && !windowInfo.tabBarView.webContents.isDestroyed()) { + windowInfo.tabBarView.setBounds({ x: 0, y: 0, width, height: TAB_BAR_HEIGHT }) + } + + // Update active tab view bounds + if (windowInfo.activeTabId) { + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (activeTab && !activeTab.view.webContents.isDestroyed()) { + activeTab.view.setBounds({ + x: 0, + y: TAB_BAR_HEIGHT, + width, + height: Math.max(0, height - TAB_BAR_HEIGHT) + }) + } + } + } + + /** + * Creates a new tab in the window + * @param privateMode - If true, uses private browsing mode (default: false) + * @param showWindow - If true, shows the browser window (default: false) + * @returns Tab ID and view + */ + public async createTab(privateMode = false, showWindow = false): Promise<{ tabId: string; view: BrowserView }> { + const windowInfo = await this.getOrCreateWindow(privateMode, showWindow) + const tabId = randomUUID() + const partition = this.getPartition(privateMode) + + const view = new BrowserView({ + webPreferences: { + contextIsolation: true, + sandbox: true, + nodeIntegration: false, + devTools: true, + partition + } + }) + + view.webContents.setUserAgent(userAgent) + + const windowKey = windowInfo.windowKey + view.webContents.on('did-start-loading', () => logger.info(`did-start-loading`, { windowKey, tabId })) + view.webContents.on('dom-ready', () => logger.info(`dom-ready`, { windowKey, tabId })) + view.webContents.on('did-finish-load', () => logger.info(`did-finish-load`, { windowKey, tabId })) + view.webContents.on('did-fail-load', (_e, code, desc) => logger.warn('Navigation failed', { code, desc })) + + view.webContents.on('destroyed', () => { + windowInfo.tabs.delete(tabId) + if (windowInfo.activeTabId === tabId) { + windowInfo.activeTabId = windowInfo.tabs.keys().next().value ?? null + if (windowInfo.activeTabId) { + const newActiveTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (newActiveTab && !windowInfo.window.isDestroyed()) { + windowInfo.window.addBrowserView(newActiveTab.view) + this.updateViewBounds(windowInfo) + } + } + } + this.sendTabBarUpdate(windowInfo) + }) + + view.webContents.on('page-title-updated', (_event, title) => { + tabInfo.title = title + this.sendTabBarUpdate(windowInfo) + }) + + view.webContents.on('did-navigate', (_event, url) => { + tabInfo.url = url + this.sendTabBarUpdate(windowInfo) + }) + + view.webContents.on('did-navigate-in-page', (_event, url) => { + tabInfo.url = url + this.sendTabBarUpdate(windowInfo) + }) + + // Handle new window requests (e.g., target="_blank" links) - open in new tab instead + view.webContents.setWindowOpenHandler(({ url }) => { + // Create a new tab and navigate to the URL + this.createTab(privateMode, true) + .then(({ tabId: newTabId }) => { + return this.switchTab(privateMode, newTabId).then(() => { + const newTab = windowInfo.tabs.get(newTabId) + if (newTab && !newTab.view.webContents.isDestroyed()) { + newTab.view.webContents.loadURL(url) + } + }) + }) + .catch((error) => { + logger.warn('Failed to open link in new tab', { error, url }) + }) + return { action: 'deny' } + }) + + const tabInfo: TabInfo = { + id: tabId, + view, + url: '', + title: '', + lastActive: Date.now() + } + + windowInfo.tabs.set(tabId, tabInfo) + + // Set as active tab and add to window + if (!windowInfo.activeTabId || windowInfo.tabs.size === 1) { + windowInfo.activeTabId = tabId + windowInfo.window.addBrowserView(view) + this.updateViewBounds(windowInfo) + } + + this.sendTabBarUpdate(windowInfo) + logger.info('Created new tab', { windowKey, tabId, privateMode }) + return { tabId, view } + } + + /** + * Gets an existing tab or creates a new one + * @param privateMode - Whether to use private browsing mode + * @param tabId - Optional specific tab ID to use + * @param newTab - If true, always create a new tab (useful for parallel requests) + * @param showWindow - If true, shows the browser window (default: false) + */ + private async getTab( + privateMode: boolean, + tabId?: string, + newTab?: boolean, + showWindow = false + ): Promise<{ tabId: string; tab: TabInfo }> { + const windowInfo = await this.getOrCreateWindow(privateMode, showWindow) + + // If newTab is requested, create a fresh tab + if (newTab) { + const { tabId: freshTabId } = await this.createTab(privateMode, showWindow) + const tab = windowInfo.tabs.get(freshTabId) + if (!tab) { + throw new Error(`Tab ${freshTabId} was created but not found - it may have been closed`) + } + return { tabId: freshTabId, tab } + } + + if (tabId) { + const tab = windowInfo.tabs.get(tabId) + if (tab && !tab.view.webContents.isDestroyed()) { + this.touchTab(windowInfo.windowKey, tabId) + return { tabId, tab } + } + } + + // Use active tab or create new one + if (windowInfo.activeTabId) { + const activeTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (activeTab && !activeTab.view.webContents.isDestroyed()) { + this.touchTab(windowInfo.windowKey, windowInfo.activeTabId) + return { tabId: windowInfo.activeTabId, tab: activeTab } + } + } + + // Create new tab + const { tabId: newTabId } = await this.createTab(privateMode, showWindow) + const tab = windowInfo.tabs.get(newTabId) + if (!tab) { + throw new Error(`Tab ${newTabId} was created but not found - it may have been closed`) + } + return { tabId: newTabId, tab } + } + /** * Opens a URL in a browser window and waits for navigation to complete. * @param url - The URL to navigate to * @param timeout - Navigation timeout in milliseconds (default: 10000) - * @param show - Whether to show the browser window (default: false) - * @param sessionId - Session identifier for window reuse (default: 'default') - * @returns Object containing the current URL and page title after navigation + * @param privateMode - If true, uses private browsing mode (default: false) + * @param newTab - If true, always creates a new tab (useful for parallel requests) + * @param showWindow - If true, shows the browser window (default: false) + * @returns Object containing the current URL, page title, and tab ID after navigation */ - public async open(url: string, timeout = 10000, show = false, sessionId = 'default') { - const win = await this.getWindow(sessionId, true, show) - logger.info('Loading URL', { url, sessionId }) - const { webContents } = win - this.touch(sessionId) + public async open(url: string, timeout = 10000, privateMode = false, newTab = false, showWindow = false) { + const { tabId: actualTabId, tab } = await this.getTab(privateMode, undefined, newTab, showWindow) + const view = tab.view + const windowKey = this.getWindowKey(privateMode) + + logger.info('Loading URL', { url, windowKey, tabId: actualTabId, privateMode }) + const { webContents } = view + this.touchTab(windowKey, actualTabId) - // Track resolution state to prevent multiple handlers from firing let resolved = false + let timeoutHandle: ReturnType | undefined let onFinish: () => void let onDomReady: () => void let onFail: (_event: Electron.Event, code: number, desc: string) => void - // Define cleanup outside Promise to ensure it's callable in finally block, - // preventing memory leaks when timeout occurs before navigation completes const cleanup = () => { + if (timeoutHandle) clearTimeout(timeoutHandle) webContents.removeListener('did-finish-load', onFinish) webContents.removeListener('did-fail-load', onFail) webContents.removeListener('dom-ready', onDomReady) @@ -192,67 +647,134 @@ export class CdpBrowserController { }) const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Navigation timed out')), timeout) + timeoutHandle = setTimeout(() => reject(new Error('Navigation timed out')), timeout) }) try { - await Promise.race([win.loadURL(url), loadPromise, timeoutPromise]) + await Promise.race([view.webContents.loadURL(url), loadPromise, timeoutPromise]) } finally { - // Always cleanup listeners to prevent memory leaks on timeout cleanup() } const currentUrl = webContents.getURL() const title = await webContents.getTitle() - return { currentUrl, title } + + // Update tab info + tab.url = currentUrl + tab.title = title + + return { currentUrl, title, tabId: actualTabId } } - public async execute(code: string, timeout = 5000, sessionId = 'default') { - const win = await this.getWindow(sessionId) - this.touch(sessionId) - const dbg = win.webContents.debugger + /** + * Executes JavaScript code in the page context using Chrome DevTools Protocol. + * @param code - JavaScript code to evaluate in the page + * @param timeout - Execution timeout in milliseconds (default: 5000) + * @param privateMode - If true, targets the private browsing window (default: false) + * @param tabId - Optional specific tab ID to target; if omitted, uses the active tab + * @returns The result value from the evaluated code, or null if no value returned + */ + public async execute(code: string, timeout = 5000, privateMode = false, tabId?: string) { + const { tabId: actualTabId, tab } = await this.getTab(privateMode, tabId) + const windowKey = this.getWindowKey(privateMode) + this.touchTab(windowKey, actualTabId) + const dbg = tab.view.webContents.debugger - await this.ensureDebuggerAttached(dbg, sessionId) + await this.ensureDebuggerAttached(dbg, windowKey) + let timeoutHandle: ReturnType | undefined const evalPromise = dbg.sendCommand('Runtime.evaluate', { expression: code, awaitPromise: true, returnByValue: true }) - const result = await Promise.race([ - evalPromise, - new Promise((_, reject) => setTimeout(() => reject(new Error('Execution timed out')), timeout)) - ]) + try { + const result = await Promise.race([ + evalPromise, + new Promise((_, reject) => { + timeoutHandle = setTimeout(() => reject(new Error('Execution timed out')), timeout) + }) + ]) - const evalResult = result as any + const evalResult = result as any - if (evalResult?.exceptionDetails) { - const message = evalResult.exceptionDetails.exception?.description || 'Unknown script error' - logger.warn('Runtime.evaluate raised exception', { message }) - throw new Error(message) + if (evalResult?.exceptionDetails) { + const message = evalResult.exceptionDetails.exception?.description || 'Unknown script error' + logger.warn('Runtime.evaluate raised exception', { message }) + throw new Error(message) + } + + const value = evalResult?.result?.value ?? evalResult?.result?.description ?? null + return value + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle) } - - const value = evalResult?.result?.value ?? evalResult?.result?.description ?? null - return value } - public async reset(sessionId?: string) { - if (sessionId) { - const entry = this.windows.get(sessionId) - if (entry) { - this.closeWindow(entry.win, sessionId) + public async reset(privateMode?: boolean, tabId?: string) { + if (privateMode !== undefined && tabId) { + const windowKey = this.getWindowKey(privateMode) + const windowInfo = this.windows.get(windowKey) + if (windowInfo) { + this.closeTabInternal(windowInfo, tabId) + windowInfo.tabs.delete(tabId) + + // If no tabs left, close the window + if (windowInfo.tabs.size === 0) { + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } + this.windows.delete(windowKey) + logger.info('Browser CDP window closed (last tab closed)', { windowKey, tabId }) + return + } + + if (windowInfo.activeTabId === tabId) { + windowInfo.activeTabId = windowInfo.tabs.keys().next().value ?? null + if (windowInfo.activeTabId) { + const newActiveTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (newActiveTab && !windowInfo.window.isDestroyed()) { + windowInfo.window.addBrowserView(newActiveTab.view) + this.updateViewBounds(windowInfo) + } + } + } + this.sendTabBarUpdate(windowInfo) } - this.windows.delete(sessionId) - logger.info('Browser CDP context reset', { sessionId }) + logger.info('Browser CDP tab reset', { windowKey, tabId }) return } - for (const [id, entry] of this.windows.entries()) { - this.closeWindow(entry.win, id) - this.windows.delete(id) + if (privateMode !== undefined) { + const windowKey = this.getWindowKey(privateMode) + const windowInfo = this.windows.get(windowKey) + if (windowInfo) { + const tabIds = Array.from(windowInfo.tabs.keys()) + for (const tid of tabIds) { + this.closeTabInternal(windowInfo, tid) + } + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } + } + this.windows.delete(windowKey) + logger.info('Browser CDP window reset', { windowKey, privateMode }) + return } - logger.info('Browser CDP context reset (all sessions)') + + const allWindowInfos = Array.from(this.windows.values()) + for (const windowInfo of allWindowInfos) { + const tabIds = Array.from(windowInfo.tabs.keys()) + for (const tid of tabIds) { + this.closeTabInternal(windowInfo, tid) + } + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.close() + } + } + this.windows.clear() + logger.info('Browser CDP context reset (all windows)') } /** @@ -260,21 +782,26 @@ export class CdpBrowserController { * @param url - The URL to fetch * @param format - Output format: 'html', 'txt', 'markdown', or 'json' (default: 'markdown') * @param timeout - Navigation timeout in milliseconds (default: 10000) - * @param sessionId - Session identifier (default: 'default') - * @returns Content in the requested format. For 'json', returns parsed object or { data: rawContent } if parsing fails + * @param privateMode - If true, uses private browsing mode (default: false) + * @param newTab - If true, always creates a new tab (useful for parallel requests) + * @param showWindow - If true, shows the browser window (default: false) + * @returns Object with tabId and content in the requested format. For 'json', content is parsed object or { data: rawContent } if parsing fails */ public async fetch( url: string, format: 'html' | 'txt' | 'markdown' | 'json' = 'markdown', timeout = 10000, - sessionId = 'default' - ) { - await this.open(url, timeout, false, sessionId) + privateMode = false, + newTab = false, + showWindow = false + ): Promise<{ tabId: string; content: string | object }> { + const { tabId } = await this.open(url, timeout, privateMode, newTab, showWindow) - const win = await this.getWindow(sessionId) - const dbg = win.webContents.debugger + const { tab } = await this.getTab(privateMode, tabId, false, showWindow) + const dbg = tab.view.webContents.debugger + const windowKey = this.getWindowKey(privateMode) - await this.ensureDebuggerAttached(dbg, sessionId) + await this.ensureDebuggerAttached(dbg, windowKey) let expression: string if (format === 'json' || format === 'txt') { @@ -283,25 +810,100 @@ export class CdpBrowserController { expression = 'document.documentElement.outerHTML' } - const result = (await dbg.sendCommand('Runtime.evaluate', { - expression, - returnByValue: true - })) as { result?: { value?: string } } + let timeoutHandle: ReturnType | undefined + try { + const result = (await Promise.race([ + dbg.sendCommand('Runtime.evaluate', { + expression, + returnByValue: true + }), + new Promise((_, reject) => { + timeoutHandle = setTimeout(() => reject(new Error('Fetch content timed out')), timeout) + }) + ])) as { result?: { value?: string } } - const content = result?.result?.value ?? '' + const rawContent = result?.result?.value ?? '' - if (format === 'markdown') { - const turndownService = new TurndownService() - return turndownService.turndown(content) + let content: string | object + if (format === 'markdown') { + content = this.turndownService.turndown(rawContent) + } else if (format === 'json') { + try { + content = JSON.parse(rawContent) + } catch (parseError) { + logger.warn('JSON parse failed, returning raw content', { + url, + contentLength: rawContent.length, + error: parseError + }) + content = { data: rawContent } + } + } else { + content = rawContent + } + + return { tabId, content } + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle) } - if (format === 'json') { - // Attempt to parse as JSON; if content is not valid JSON, wrap it in a data object - try { - return JSON.parse(content) - } catch { - return { data: content } + } + + /** + * Lists all tabs in a window + * @param privateMode - If true, lists tabs from private window (default: false) + */ + public async listTabs(privateMode = false): Promise> { + const windowKey = this.getWindowKey(privateMode) + const windowInfo = this.windows.get(windowKey) + if (!windowInfo) return [] + + return Array.from(windowInfo.tabs.values()).map((tab) => ({ + tabId: tab.id, + url: tab.url, + title: tab.title + })) + } + + /** + * Closes a specific tab + * @param privateMode - If true, closes tab from private window (default: false) + * @param tabId - Tab identifier to close + */ + public async closeTab(privateMode: boolean, tabId: string) { + await this.reset(privateMode, tabId) + } + + /** + * Switches the active tab + * @param privateMode - If true, switches tab in private window (default: false) + * @param tabId - Tab identifier to switch to + */ + public async switchTab(privateMode: boolean, tabId: string) { + const windowKey = this.getWindowKey(privateMode) + const windowInfo = this.windows.get(windowKey) + if (!windowInfo) throw new Error(`Window not found for ${privateMode ? 'private' : 'normal'} mode`) + + const tab = windowInfo.tabs.get(tabId) + if (!tab) throw new Error(`Tab ${tabId} not found`) + + // Remove previous active tab view (but NOT the tabBarView) + if (windowInfo.activeTabId && windowInfo.activeTabId !== tabId) { + const prevTab = windowInfo.tabs.get(windowInfo.activeTabId) + if (prevTab && !windowInfo.window.isDestroyed()) { + windowInfo.window.removeBrowserView(prevTab.view) } } - return content + + windowInfo.activeTabId = tabId + + // Add the new active tab view + if (!windowInfo.window.isDestroyed()) { + windowInfo.window.addBrowserView(tab.view) + this.updateViewBounds(windowInfo) + } + + this.touchTab(windowKey, tabId) + this.sendTabBarUpdate(windowInfo) + logger.info('Switched active tab', { windowKey, tabId, privateMode }) } } diff --git a/src/main/mcpServers/browser/tabbar-html.ts b/src/main/mcpServers/browser/tabbar-html.ts new file mode 100644 index 0000000000..4a1bec0e0d --- /dev/null +++ b/src/main/mcpServers/browser/tabbar-html.ts @@ -0,0 +1,567 @@ +export const TAB_BAR_HTML = ` + + + + + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ + + +
+ +
+
+ + +` diff --git a/src/main/mcpServers/browser/tools/execute.ts b/src/main/mcpServers/browser/tools/execute.ts index 1585a467a8..09cd79f2d1 100644 --- a/src/main/mcpServers/browser/tools/execute.ts +++ b/src/main/mcpServers/browser/tools/execute.ts @@ -1,36 +1,39 @@ import * as z from 'zod' import type { CdpBrowserController } from '../controller' +import { logger } from '../types' import { errorResponse, successResponse } from './utils' export const ExecuteSchema = z.object({ - code: z - .string() - .describe( - 'JavaScript evaluated via Chrome DevTools Runtime.evaluate. Keep it short; prefer one-line with semicolons for multiple statements.' - ), - timeout: z.number().default(5000).describe('Timeout in milliseconds for code execution (default: 5000ms)'), - sessionId: z.string().optional().describe('Session identifier to target a specific page (default: default)') + code: z.string().describe('JavaScript code to run in page context'), + timeout: z.number().default(5000).describe('Execution timeout in ms (default: 5000)'), + privateMode: z.boolean().optional().describe('Target private session (default: false)'), + tabId: z.string().optional().describe('Target specific tab by ID') }) export const executeToolDefinition = { name: 'execute', description: - 'Run JavaScript in the current page via Runtime.evaluate. Prefer short, single-line snippets; use semicolons for multiple statements.', + 'Run JavaScript in the currently open page. Use after open to: click elements, fill forms, extract content (document.body.innerText), or interact with the page. The page must be opened first with open or fetch.', inputSchema: { type: 'object', properties: { code: { type: 'string', - description: 'One-line JS to evaluate in page context' + description: + 'JavaScript to evaluate. Examples: document.body.innerText (get text), document.querySelector("button").click() (click), document.title (get title)' }, timeout: { type: 'number', - description: 'Timeout in milliseconds (default 5000)' + description: 'Execution timeout in ms (default: 5000)' }, - sessionId: { + privateMode: { + type: 'boolean', + description: 'Target private session (default: false)' + }, + tabId: { type: 'string', - description: 'Session identifier; targets a specific page (default: default)' + description: 'Target specific tab by ID (from open response)' } }, required: ['code'] @@ -38,11 +41,12 @@ export const executeToolDefinition = { } export async function handleExecute(controller: CdpBrowserController, args: unknown) { - const { code, timeout, sessionId } = ExecuteSchema.parse(args) + const { code, timeout, privateMode, tabId } = ExecuteSchema.parse(args) try { - const value = await controller.execute(code, timeout, sessionId ?? 'default') + const value = await controller.execute(code, timeout, privateMode ?? false, tabId) return successResponse(typeof value === 'string' ? value : JSON.stringify(value)) } catch (error) { + logger.error('Execute failed', { error, code: code.slice(0, 100), privateMode, tabId }) return errorResponse(error as Error) } } diff --git a/src/main/mcpServers/browser/tools/fetch.ts b/src/main/mcpServers/browser/tools/fetch.ts deleted file mode 100644 index b749aaff93..0000000000 --- a/src/main/mcpServers/browser/tools/fetch.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as z from 'zod' - -import type { CdpBrowserController } from '../controller' -import { errorResponse, successResponse } from './utils' - -export const FetchSchema = z.object({ - url: z.url().describe('URL to fetch'), - format: z.enum(['html', 'txt', 'markdown', 'json']).default('markdown').describe('Output format (default: markdown)'), - timeout: z.number().optional().describe('Timeout in milliseconds for navigation (default: 10000)'), - sessionId: z.string().optional().describe('Session identifier (default: default)') -}) - -export const fetchToolDefinition = { - name: 'fetch', - description: 'Fetch a URL using the browser and return content in specified format (html, txt, markdown, json)', - inputSchema: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'URL to fetch' - }, - format: { - type: 'string', - enum: ['html', 'txt', 'markdown', 'json'], - description: 'Output format (default: markdown)' - }, - timeout: { - type: 'number', - description: 'Navigation timeout in milliseconds (default: 10000)' - }, - sessionId: { - type: 'string', - description: 'Session identifier (default: default)' - } - }, - required: ['url'] - } -} - -export async function handleFetch(controller: CdpBrowserController, args: unknown) { - const { url, format, timeout, sessionId } = FetchSchema.parse(args) - try { - const content = await controller.fetch(url, format, timeout ?? 10000, sessionId ?? 'default') - return successResponse(typeof content === 'string' ? content : JSON.stringify(content)) - } catch (error) { - return errorResponse(error as Error) - } -} diff --git a/src/main/mcpServers/browser/tools/index.ts b/src/main/mcpServers/browser/tools/index.ts index 19f1ee4163..5ba6fcae6d 100644 --- a/src/main/mcpServers/browser/tools/index.ts +++ b/src/main/mcpServers/browser/tools/index.ts @@ -1,15 +1,13 @@ export { ExecuteSchema, executeToolDefinition, handleExecute } from './execute' -export { FetchSchema, fetchToolDefinition, handleFetch } from './fetch' export { handleOpen, OpenSchema, openToolDefinition } from './open' export { handleReset, resetToolDefinition } from './reset' import type { CdpBrowserController } from '../controller' import { executeToolDefinition, handleExecute } from './execute' -import { fetchToolDefinition, handleFetch } from './fetch' import { handleOpen, openToolDefinition } from './open' import { handleReset, resetToolDefinition } from './reset' -export const toolDefinitions = [openToolDefinition, executeToolDefinition, resetToolDefinition, fetchToolDefinition] +export const toolDefinitions = [openToolDefinition, executeToolDefinition, resetToolDefinition] export const toolHandlers: Record< string, @@ -20,6 +18,5 @@ export const toolHandlers: Record< > = { open: handleOpen, execute: handleExecute, - reset: handleReset, - fetch: handleFetch + reset: handleReset } diff --git a/src/main/mcpServers/browser/tools/open.ts b/src/main/mcpServers/browser/tools/open.ts index 9739b3bcae..6ea9ec9e48 100644 --- a/src/main/mcpServers/browser/tools/open.ts +++ b/src/main/mcpServers/browser/tools/open.ts @@ -1,39 +1,52 @@ import * as z from 'zod' import type { CdpBrowserController } from '../controller' -import { successResponse } from './utils' +import { logger } from '../types' +import { errorResponse, successResponse } from './utils' export const OpenSchema = z.object({ - url: z.url().describe('URL to open in the controlled Electron window'), - timeout: z.number().optional().describe('Timeout in milliseconds for navigation (default: 10000)'), - show: z.boolean().optional().describe('Whether to show the browser window (default: false)'), - sessionId: z - .string() + url: z.url().describe('URL to navigate to'), + format: z + .enum(['html', 'txt', 'markdown', 'json']) .optional() - .describe('Session identifier; separate sessions keep separate pages (default: default)') + .describe('If set, return page content in this format. If not set, just open the page and return tabId.'), + timeout: z.number().optional().describe('Navigation timeout in ms (default: 10000)'), + privateMode: z.boolean().optional().describe('Use incognito mode, no data persisted (default: false)'), + newTab: z.boolean().optional().describe('Open in new tab, required for parallel requests (default: false)'), + showWindow: z.boolean().optional().default(true).describe('Show browser window (default: true)') }) export const openToolDefinition = { name: 'open', - description: 'Open a URL in a hidden Electron window controlled via Chrome DevTools Protocol', + description: + 'Navigate to a URL in a browser window. If format is specified, returns { tabId, content } with page content in that format. Otherwise, returns { currentUrl, title, tabId } for subsequent operations with execute tool. Set newTab=true when opening multiple URLs in parallel.', inputSchema: { type: 'object', properties: { url: { type: 'string', - description: 'URL to load' + description: 'URL to navigate to' + }, + format: { + type: 'string', + enum: ['html', 'txt', 'markdown', 'json'], + description: 'If set, return page content in this format. If not set, just open the page and return tabId.' }, timeout: { type: 'number', - description: 'Navigation timeout in milliseconds (default 10000)' + description: 'Navigation timeout in ms (default: 10000)' }, - show: { + privateMode: { type: 'boolean', - description: 'Whether to show the browser window (default false)' + description: 'Use incognito mode, no data persisted (default: false)' }, - sessionId: { - type: 'string', - description: 'Session identifier; separate sessions keep separate pages (default: default)' + newTab: { + type: 'boolean', + description: 'Open in new tab, required for parallel requests (default: false)' + }, + showWindow: { + type: 'boolean', + description: 'Show browser window (default: true)' } }, required: ['url'] @@ -41,7 +54,28 @@ export const openToolDefinition = { } export async function handleOpen(controller: CdpBrowserController, args: unknown) { - const { url, timeout, show, sessionId } = OpenSchema.parse(args) - const res = await controller.open(url, timeout ?? 10000, show ?? false, sessionId ?? 'default') - return successResponse(JSON.stringify(res)) + try { + const { url, format, timeout, privateMode, newTab, showWindow } = OpenSchema.parse(args) + + if (format) { + const { tabId, content } = await controller.fetch( + url, + format, + timeout ?? 10000, + privateMode ?? false, + newTab ?? false, + showWindow + ) + return successResponse(JSON.stringify({ tabId, content })) + } else { + const res = await controller.open(url, timeout ?? 10000, privateMode ?? false, newTab ?? false, showWindow) + return successResponse(JSON.stringify(res)) + } + } catch (error) { + logger.error('Open failed', { + error, + url: args && typeof args === 'object' && 'url' in args ? args.url : undefined + }) + return errorResponse(error instanceof Error ? error : String(error)) + } } diff --git a/src/main/mcpServers/browser/tools/reset.ts b/src/main/mcpServers/browser/tools/reset.ts index d09d251119..fe67b74b1d 100644 --- a/src/main/mcpServers/browser/tools/reset.ts +++ b/src/main/mcpServers/browser/tools/reset.ts @@ -1,34 +1,43 @@ import * as z from 'zod' import type { CdpBrowserController } from '../controller' -import { successResponse } from './utils' +import { logger } from '../types' +import { errorResponse, successResponse } from './utils' -/** Zod schema for validating reset tool arguments */ export const ResetSchema = z.object({ - sessionId: z.string().optional().describe('Session identifier to reset; omit to reset all sessions') + privateMode: z.boolean().optional().describe('true=private window, false=normal window, omit=all windows'), + tabId: z.string().optional().describe('Close specific tab only (requires privateMode)') }) -/** MCP tool definition for the reset tool */ export const resetToolDefinition = { name: 'reset', - description: 'Reset the controlled window and detach debugger', + description: + 'Close browser windows and clear state. Call when done browsing to free resources. Omit all parameters to close everything.', inputSchema: { type: 'object', properties: { - sessionId: { + privateMode: { + type: 'boolean', + description: 'true=reset private window only, false=reset normal window only, omit=reset all' + }, + tabId: { type: 'string', - description: 'Session identifier to reset; omit to reset all sessions' + description: 'Close specific tab only (requires privateMode to be set)' } } } } -/** - * Handler for the reset MCP tool. - * Closes browser window(s) and detaches debugger for the specified session or all sessions. - */ export async function handleReset(controller: CdpBrowserController, args: unknown) { - const { sessionId } = ResetSchema.parse(args) - await controller.reset(sessionId) - return successResponse('reset') + try { + const { privateMode, tabId } = ResetSchema.parse(args) + await controller.reset(privateMode, tabId) + return successResponse('reset') + } catch (error) { + logger.error('Reset failed', { + error, + privateMode: args && typeof args === 'object' && 'privateMode' in args ? args.privateMode : undefined + }) + return errorResponse(error instanceof Error ? error : String(error)) + } } diff --git a/src/main/mcpServers/browser/tools/utils.ts b/src/main/mcpServers/browser/tools/utils.ts index 2c5ecc0f1d..f5272ac81c 100644 --- a/src/main/mcpServers/browser/tools/utils.ts +++ b/src/main/mcpServers/browser/tools/utils.ts @@ -5,9 +5,10 @@ export function successResponse(text: string) { } } -export function errorResponse(error: Error) { +export function errorResponse(error: Error | string) { + const message = error instanceof Error ? error.message : error return { - content: [{ type: 'text', text: error.message }], + content: [{ type: 'text', text: message }], isError: true } } diff --git a/src/main/mcpServers/browser/types.ts b/src/main/mcpServers/browser/types.ts index 2cc934e6ce..a59fe59665 100644 --- a/src/main/mcpServers/browser/types.ts +++ b/src/main/mcpServers/browser/types.ts @@ -1,4 +1,24 @@ import { loggerService } from '@logger' +import type { BrowserView, BrowserWindow } from 'electron' export const logger = loggerService.withContext('MCPBrowserCDP') -export const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0' +export const userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + +export interface TabInfo { + id: string + view: BrowserView + url: string + title: string + lastActive: number +} + +export interface WindowInfo { + windowKey: string + privateMode: boolean + window: BrowserWindow + tabs: Map + activeTabId: string | null + lastActive: number + tabBarView?: BrowserView +} diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 3f96497e63..cda99cc37a 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -255,6 +255,12 @@ export class WindowService { } private setupWebContentsHandlers(mainWindow: BrowserWindow) { + // Fix for Electron bug where zoom resets during in-page navigation (route changes) + // This complements the resize-based workaround by catching navigation events + mainWindow.webContents.on('did-navigate-in-page', () => { + mainWindow.webContents.setZoomFactor(configManager.getZoomFactor()) + }) + mainWindow.webContents.on('will-navigate', (event, url) => { if (url.includes('localhost:517')) { return @@ -516,7 +522,9 @@ export class WindowService { miniWindowState.manage(this.miniWindow) //miniWindow should show in current desktop - this.miniWindow?.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + this.miniWindow?.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true + }) //make miniWindow always on top of fullscreen apps with level set //[mac] level higher than 'floating' will cover the pinyin input method this.miniWindow.setAlwaysOnTop(true, 'floating') @@ -635,6 +643,11 @@ export class WindowService { return } else if (isMac) { this.miniWindow.hide() + const majorVersion = parseInt(process.getSystemVersion().split('.')[0], 10) + if (majorVersion >= 26) { + // on macOS 26+, the popup of the mimiWindow would not change the focus to previous application. + return + } if (!this.wasMainWindowFocused) { app.hide() } diff --git a/src/main/utils/locales.ts b/src/main/utils/locales.ts index b41cba7c75..afaf48b20f 100644 --- a/src/main/utils/locales.ts +++ b/src/main/utils/locales.ts @@ -8,6 +8,7 @@ import esES from '../../renderer/src/i18n/translate/es-es.json' import frFR from '../../renderer/src/i18n/translate/fr-fr.json' import JaJP from '../../renderer/src/i18n/translate/ja-jp.json' import ptPT from '../../renderer/src/i18n/translate/pt-pt.json' +import roRO from '../../renderer/src/i18n/translate/ro-ro.json' import RuRu from '../../renderer/src/i18n/translate/ru-ru.json' const locales = Object.fromEntries( @@ -21,7 +22,8 @@ const locales = Object.fromEntries( ['el-GR', elGR], ['es-ES', esES], ['fr-FR', frFR], - ['pt-PT', ptPT] + ['pt-PT', ptPT], + ['ro-RO', roRO] ].map(([locale, translation]) => [locale, { translation }]) ) diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index 8346eee120..a51b718ecf 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -222,6 +222,7 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht afterClose={onClose} centered={!isFullscreen} destroyOnHidden + forceRender={isFullscreen} mask={!isFullscreen} maskClosable={false} width={isFullscreen ? '100vw' : '90vw'} diff --git a/src/renderer/src/components/EmojiPicker/index.tsx b/src/renderer/src/components/EmojiPicker/index.tsx index 9a4158d469..c0a21e7c3c 100644 --- a/src/renderer/src/components/EmojiPicker/index.tsx +++ b/src/renderer/src/components/EmojiPicker/index.tsx @@ -45,6 +45,7 @@ const i18nMap: Record = { 'fr-FR': fr, 'ja-JP': ja, 'pt-PT': pt_PT, + 'ro-RO': en, // No Romanian available, fallback to English 'ru-RU': ru_RU } @@ -60,6 +61,7 @@ const dataSourceMap: Record = { 'fr-FR': dataFR, 'ja-JP': dataJA, 'pt-PT': dataPT, + 'ro-RO': dataEN, // No Romanian CLDR available, fallback to English 'ru-RU': dataRU } @@ -75,6 +77,7 @@ const localeMap: Record = { 'fr-FR': 'fr', 'ja-JP': 'ja', 'pt-PT': 'pt', + 'ro-RO': 'en', 'ru-RU': 'ru' } diff --git a/src/renderer/src/context/AntdProvider.tsx b/src/renderer/src/context/AntdProvider.tsx index 685b3b0fbd..7fa1fbd79a 100644 --- a/src/renderer/src/context/AntdProvider.tsx +++ b/src/renderer/src/context/AntdProvider.tsx @@ -8,6 +8,7 @@ import esES from 'antd/locale/es_ES' import frFR from 'antd/locale/fr_FR' import jaJP from 'antd/locale/ja_JP' import ptPT from 'antd/locale/pt_PT' +import roRO from 'antd/locale/ro_RO' import ruRU from 'antd/locale/ru_RU' import zhCN from 'antd/locale/zh_CN' import zhTW from 'antd/locale/zh_TW' @@ -141,6 +142,8 @@ function getAntdLocale(language: LanguageVarious) { return frFR case 'pt-PT': return ptPT + case 'ro-RO': + return roRO default: return zhCN } diff --git a/src/renderer/src/hooks/useSmoothStream.ts b/src/renderer/src/hooks/useSmoothStream.ts index 2fffd92b8f..0c96f1b25e 100644 --- a/src/renderer/src/hooks/useSmoothStream.ts +++ b/src/renderer/src/hooks/useSmoothStream.ts @@ -7,7 +7,7 @@ interface UseSmoothStreamOptions { initialText?: string } -const languages = ['en-US', 'de-DE', 'es-ES', 'zh-CN', 'zh-TW', 'ja-JP', 'ru-RU', 'el-GR', 'fr-FR', 'pt-PT'] +const languages = ['en-US', 'de-DE', 'es-ES', 'zh-CN', 'zh-TW', 'ja-JP', 'ru-RU', 'el-GR', 'fr-FR', 'pt-PT', 'ro-RO'] const segmenter = new Intl.Segmenter(languages) export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialText = '' }: UseSmoothStreamOptions) => { diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts index 6191560d3d..8b4f7147bb 100644 --- a/src/renderer/src/hooks/useTranslate.ts +++ b/src/renderer/src/hooks/useTranslate.ts @@ -36,18 +36,16 @@ export default function useTranslate() { const getLanguageByLangcode = useCallback( (langCode: string) => { - if (!isLoaded) { - logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.') - return UNKNOWN - } - const result = translateLanguages.find((item) => item.langCode === langCode) + if (result) { return result + } else if (!isLoaded) { + logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.') } else { logger.warn(`Unknown language ${langCode}`) - return UNKNOWN } + return UNKNOWN }, [isLoaded, translateLanguages] ) @@ -63,6 +61,7 @@ export default function useTranslate() { prompt, settings, translateLanguages, + isLoaded, getLanguageByLangcode, updateSettings: handleUpdateSettings } diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index f2e6e7424f..5b8e1cc7ac 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -14,6 +14,7 @@ import esES from './translate/es-es.json' import frFR from './translate/fr-fr.json' import jaJP from './translate/ja-jp.json' import ptPT from './translate/pt-pt.json' +import roRO from './translate/ro-ro.json' import ruRU from './translate/ru-ru.json' const logger = loggerService.withContext('I18N') @@ -29,7 +30,8 @@ const resources = Object.fromEntries( ['el-GR', elGR], ['es-ES', esES], ['fr-FR', frFR], - ['pt-PT', ptPT] + ['pt-PT', ptPT], + ['ro-RO', roRO] ].map(([locale, translation]) => [locale, { translation }]) ) diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json new file mode 100644 index 0000000000..6002365814 --- /dev/null +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -0,0 +1,5101 @@ +{ + "agent": { + "add": { + "description": "Gestionează sarcini complexe cu diverse instrumente", + "error": { + "failed": "Nu s-a putut adăuga un agent", + "invalid_agent": "Agent invalid" + }, + "model": { + "tooltip": "Momentan, doar modelele care acceptă endpoint-uri Anthropic sunt disponibile pentru funcția Agent." + }, + "title": "Adaugă agent", + "type": { + "placeholder": "Selectează un tip de agent" + } + }, + "delete": { + "content": "Ștergerea agentului va opri forțat și va șterge toate sesiunile asociate cu agentul. Ești sigur?", + "error": { + "failed": "Nu s-a putut șterge agentul" + }, + "title": "Șterge agentul" + }, + "edit": { + "title": "Editează agentul" + }, + "get": { + "error": { + "failed": "Nu s-a putut obține agentul.", + "null_id": "ID-ul agentului este nul." + } + }, + "gitBash": { + "autoDetected": "Se folosește Git Bash detectat automat", + "autoDiscoveredHint": "Descoperit automat", + "clear": { + "button": "Șterge calea personalizată" + }, + "customPath": "Se folosește calea personalizată: {{path}}", + "error": { + "description": "Git Bash este necesar pentru a rula agenți pe Windows. Agentul nu poate funcționa fără acesta. Te rugăm să instalezi Git pentru Windows de la", + "recheck": "Verifică din nou instalarea Git Bash", + "required": "Calea Git Bash este necesară pe Windows", + "title": "Git Bash este necesar" + }, + "found": { + "title": "Git Bash configurat" + }, + "notFound": "Git Bash nu a fost găsit. Te rugăm să-l instalezi mai întâi.", + "pick": { + "button": "Selectează calea Git Bash", + "failed": "Nu s-a putut seta calea Git Bash", + "invalidPath": "Fișierul selectat nu este un executabil Git Bash valid (bash.exe).", + "title": "Selectează executabilul Git Bash" + }, + "placeholder": "Selectează calea bash.exe", + "success": "Git Bash a fost detectat cu succes!", + "tooltip": "Git Bash este necesar pentru a rula agenți pe Windows. Instalează-l de pe git-scm.com dacă nu este disponibil." + }, + "input": { + "placeholder": "Introdu mesajul aici, trimite cu {{key}} - @ selectează calea, / selectează comanda" + }, + "list": { + "error": { + "failed": "Nu s-a putut afișa lista de agenți." + } + }, + "server": { + "error": { + "not_running": "Serverul API este activat, dar nu rulează corect." + } + }, + "session": { + "accessible_paths": { + "add": "Adaugă director", + "duplicate": "Acest director este deja inclus.", + "empty": "Selectează cel puțin un director pe care agentul îl poate accesa.", + "error": { + "at_least_one": "Te rugăm să selectezi cel puțin un director accesibil." + }, + "label": "Directoare accesibile", + "select_failed": "Nu s-a putut selecta directorul." + }, + "add": { + "title": "Adaugă o sesiune" + }, + "allowed_tools": { + "empty": "Niciun instrument disponibil pentru acest agent.", + "helper": "Instrumentele pre-aprobate rulează fără aprobare manuală. Instrumentele neselectate necesită aprobare înainte de utilizare.", + "label": "Instrumente pre-aprobate", + "placeholder": "Selectează instrumente pre-aprobate" + }, + "create": { + "error": { + "failed": "Nu s-a putut adăuga o sesiune" + } + }, + "delete": { + "content": "Ești sigur că vrei să ștergi această sesiune?", + "error": { + "failed": "Nu s-a putut șterge sesiunea", + "last": "Trebuie păstrată cel puțin o sesiune" + }, + "title": "Șterge sesiunea" + }, + "edit": { + "title": "Editează sesiunea" + }, + "get": { + "error": { + "failed": "Nu s-a putut obține sesiunea", + "null_id": "ID-ul sesiunii este nul" + } + }, + "label_one": "Sesiune", + "label_other": "Sesiuni", + "update": { + "error": { + "failed": "Nu s-a putut actualiza sesiunea" + } + } + }, + "settings": { + "advance": { + "maxTurns": { + "description": "Definește câte cicluri cerere/răspuns poate finaliza automat agentul.", + "helper": "Valorile mai mari permit rulări autonome mai lungi; valorile mai mici mențin sesiunile scurte.", + "label": "Limită de schimburi în conversație" + }, + "permissionMode": { + "description": "Controlează modul în care agentul gestionează acțiunile care necesită aprobare.", + "label": "Mod de permisiune", + "options": { + "acceptEdits": "Acceptă automat editările", + "bypassPermissions": "Omite verificările de permisiune", + "default": "Implicit (întreabă înainte de a continua)", + "plan": "Mod de planificare (necesită aprobarea planului)" + }, + "placeholder": "Alege un comportament pentru permisiuni" + }, + "title": "Setări avansate" + }, + "essential": "Setări esențiale", + "plugins": { + "available": { + "title": "Pluginuri disponibile" + }, + "confirm": { + "uninstall": "Ești sigur că vrei să dezinstalezi acest plugin?" + }, + "empty": { + "available": "Nu s-au găsit pluginuri care să corespundă filtrelor tale. Încearcă să ajustezi căutarea sau filtrele de categorie." + }, + "error": { + "install": "Nu s-a putut instala pluginul", + "load": "Nu s-au putut încărca pluginurile", + "uninstall": "Nu s-a putut dezinstala pluginul" + }, + "filter": { + "all": "Toate categoriile" + }, + "install": "Instalează", + "installed": { + "empty": "Niciun plugin instalat încă. Răsfoiește pluginurile disponibile pentru a începe.", + "title": "Pluginuri instalate" + }, + "installing": "Se instalează...", + "results": "{{count}} plugin(uri) găsit(e)", + "search": { + "placeholder": "Caută pluginuri..." + }, + "success": { + "install": "Plugin instalat cu succes", + "uninstall": "Plugin dezinstalat cu succes" + }, + "tab": "Pluginuri", + "type": { + "agent": "Agent", + "agents": "Agenți", + "all": "Toate", + "command": "Comandă", + "commands": "Comenzi", + "skills": "Abilități" + }, + "uninstall": "Dezinstalează", + "uninstalling": "Se dezinstalează..." + }, + "prompt": "Setări prompt", + "tooling": { + "mcp": { + "description": "Conectează servere MCP pentru a debloca instrumente suplimentare pe care le poți aproba mai sus.", + "empty": "Nu au fost detectate servere MCP. Adaugă unul din pagina de setări MCP.", + "manageHint": "Ai nevoie de configurare avansată? Vizitează Setări → Servere MCP.", + "toggle": "Comută {{name}}" + }, + "permissionMode": { + "acceptEdits": { + "behavior": "Pre-aprobă instrumentele de sistem de fișiere de încredere, astfel încât editările să ruleze imediat.", + "description": "Editările de fișiere și operațiunile sistemului de fișiere sunt aprobate automat.", + "title": "Acceptă automat editările de fișiere" + }, + "bypassPermissions": { + "behavior": "Fiecare instrument este pre-aprobat automat.", + "description": "Toate solicitările de permisiune sunt omise — folosește cu precauție.", + "title": "Omite verificările de permisiune", + "warning": "Folosește cu precauție — toate instrumentele vor rula fără a cere aprobare." + }, + "confirmChange": { + "description": "Schimbarea modurilor actualizează instrumentele aprobate automat.", + "title": "Schimbi modul de permisiune?" + }, + "default": { + "behavior": "Instrumentele doar-pentru-citire sunt pre-aprobate automat.", + "description": "Instrumentele doar-pentru-citire sunt pre-aprobate; orice altceva necesită încă permisiune.", + "title": "Implicit (întreabă înainte de a continua)" + }, + "helper": "Alege cum gestionează agentul aprobările pentru instrumente.", + "placeholder": "Selectează modul de permisiune", + "plan": { + "behavior": "Setările implicite doar-pentru-citire sunt pre-aprobate, în timp ce execuția rămâne dezactivată.", + "description": "Partajează setul implicit de instrumente doar-pentru-citire, dar prezintă un plan înainte de execuție.", + "title": "Mod de planificare" + }, + "title": "Mod de permisiune" + }, + "preapproved": { + "autoBadge": "Adăugat de mod", + "autoDescription": "Acest instrument este aprobat automat de modul de permisiune curent.", + "empty": "Niciun instrument nu corespunde filtrelor tale.", + "mcpBadge": "Instrument MCP", + "requiresApproval": "Necesită aprobare când este dezactivat", + "search": "Caută instrumente", + "toggle": "Comută {{name}}", + "warning": { + "description": "Activează doar instrumentele în care ai încredere. Setările implicite ale modului sunt evidențiate automat.", + "title": "Instrumentele pre-aprobate rulează fără revizuire manuală." + } + }, + "review": { + "autoTools": "Auto: {{count}}", + "customTools": "Personalizat: {{count}}", + "helper": "Modificările se salvează automat. Ajustează pașii de mai sus oricând pentru a regla fin permisiunile.", + "mcp": "MCP: {{count}}", + "mode": "Mod: {{mode}}" + }, + "steps": { + "mcp": { + "title": "Servere MCP" + }, + "permissionMode": { + "title": "Pasul 1 · Mod de permisiune" + }, + "preapproved": { + "title": "Pasul 2 · Instrumente pre-aprobate" + }, + "review": { + "title": "Pasul 3 · Revizuire" + } + }, + "tab": "Instrumente și permisiuni" + }, + "tools": { + "approved": "aprobat", + "caution": "Instrumentele pre-aprobate ocolesc revizuirea umană. Activează doar instrumente de încredere.", + "description": "Alege ce instrumente pot rula fără aprobare manuală.", + "requiresPermission": "Necesită permisiune când nu este pre-aprobat.", + "tab": "Instrumente pre-aprobate", + "title": "Instrumente pre-aprobate", + "toggle": "{{defaultValue}}" + } + }, + "toolPermission": { + "aria": { + "allowRequest": "Permite cererea instrumentului", + "denyRequest": "Refuză cererea instrumentului", + "hideDetails": "Ascunde detaliile instrumentului", + "runWithOptions": "Rulează cu opțiuni suplimentare", + "showDetails": "Arată detaliile instrumentului" + }, + "button": { + "cancel": "Anulează", + "run": "Rulează" + }, + "confirmation": "Ești sigur că vrei să rulezi acest instrument Claude?", + "defaultDenyMessage": "Utilizatorul a refuzat permisiunea pentru acest instrument.", + "defaultDescription": "Execută cod sau acțiuni de sistem în mediul tău. Asigură-te că comanda pare sigură înainte de a o rula.", + "error": { + "sendFailed": "Nu s-a putut trimite decizia ta. Te rugăm să încerci din nou." + }, + "executing": "Se execută...", + "expired": "Expirat", + "inputPreview": "Previzualizare intrare instrument", + "pending": "În așteptare ({{seconds}}s)", + "permissionExpired": "Cererea de permisiune a expirat. Se așteaptă instrucțiuni noi...", + "requiresElevatedPermissions": "Acest instrument necesită permisiuni elevate.", + "suggestion": { + "permissionUpdateMultiple": "Aprobarea poate actualiza permisiunile mai multor sesiuni dacă ai ales să permiți întotdeauna acest instrument.", + "permissionUpdateSingle": "Aprobarea poate actualiza permisiunile sesiunii tale dacă ai ales să permiți întotdeauna acest instrument." + }, + "toast": { + "denied": "Cererea instrumentului a fost refuzată.", + "timeout": "Cererea instrumentului a expirat înainte de a primi aprobare." + }, + "toolPendingFallback": "Instrument", + "waiting": "Se așteaptă decizia privind permisiunea instrumentului..." + }, + "type": { + "label": "Tip agent", + "unknown": "Tip necunoscut" + }, + "update": { + "error": { + "failed": "Nu s-a putut actualiza agentul" + } + }, + "warning": { + "enable_server": "Activează serverul API pentru a folosi agenți." + } + }, + "apiServer": { + "actions": { + "copy": "Copiază", + "regenerate": "Regenerează", + "restart": { + "button": "Repornește", + "tooltip": "Repornește serverul" + }, + "start": "Pornește", + "stop": "Oprește" + }, + "authHeader": { + "title": "Header de autorizare" + }, + "authHeaderText": "Utilizează în header-ul Authorization:", + "configuration": "Configurare", + "description": "Expune capacitățile AI ale Cherry Studio prin API-uri HTTP compatibile cu OpenAI", + "documentation": { + "title": "Documentație API" + }, + "fields": { + "apiKey": { + "copyTooltip": "Copiază cheia API", + "description": "Token de autentificare securizat pentru acces API", + "label": "Cheie API", + "placeholder": "Cheia API va fi generată automat" + }, + "port": { + "description": "Numărul portului TCP pentru serverul HTTP (1000-65535)", + "helpText": "Oprește serverul pentru a schimba portul", + "label": "Port" + }, + "url": { + "copyTooltip": "Copiază URL-ul", + "label": "URL" + } + }, + "messages": { + "apiKeyCopied": "Cheia API a fost copiată în clipboard", + "apiKeyRegenerated": "Cheia API a fost regenerată", + "notEnabled": "Serverul API nu este activat.", + "operationFailed": "Operațiunea serverului API a eșuat: ", + "restartError": "Nu s-a putut reporni serverul API: ", + "restartFailed": "Repornirea serverului API a eșuat: ", + "restartSuccess": "Serverul API a repornit cu succes", + "startError": "Nu s-a putut porni serverul API: ", + "startSuccess": "Serverul API a pornit cu succes", + "stopError": "Nu s-a putut opri serverul API: ", + "stopSuccess": "Serverul API s-a oprit cu succes", + "urlCopied": "URL-ul serverului a fost copiat în clipboard" + }, + "status": { + "running": "Rulează", + "stopped": "Oprit" + }, + "title": "Server API" + }, + "appMenu": { + "about": "Despre", + "close": "Închide fereastra", + "copy": "Copiază", + "cut": "Taie", + "delete": "Șterge", + "documentation": "Documentație", + "edit": "Editare", + "feedback": "Feedback", + "file": "Fișier", + "forceReload": "Reîncărcare forțată", + "front": "Adu toate în față", + "help": "Ajutor", + "hide": "Ascunde", + "hideOthers": "Ascunde celelalte", + "minimize": "Minimizează", + "paste": "Lipește", + "quit": "Ieșire", + "redo": "Refă", + "releases": "Lansări", + "reload": "Reîncarcă", + "resetZoom": "Dimensiune reală", + "selectAll": "Selectează tot", + "services": "Servicii", + "toggleDevTools": "Comută instrumentele pentru dezvoltatori", + "toggleFullscreen": "Comută ecranul complet", + "undo": "Anulează", + "unhide": "Arată toate", + "view": "Vizualizare", + "website": "Site web", + "window": "Fereastră", + "zoom": "Zoom", + "zoomIn": "Mărește", + "zoomOut": "Micșorează" + }, + "assistants": { + "abbr": "Asistenți", + "clear": { + "content": "Golirea subiectului va șterge toate subiectele și fișierele din asistent. Ești sigur că vrei să continui?", + "title": "Șterge subiectele" + }, + "copy": { + "title": "Copiază asistentul" + }, + "delete": { + "content": "Ștergerea unui asistent va șterge toate subiectele și fișierele din cadrul asistentului. Ești sigur că vrei să-l ștergi?", + "error": { + "remain_one": "Nu este permisă ștergerea ultimului" + }, + "title": "Șterge asistentul" + }, + "edit": { + "title": "Editează asistentul" + }, + "icon": { + "type": "Pictogramă asistent" + }, + "list": { + "showByList": "Vizualizare listă", + "showByTags": "Vizualizare etichete" + }, + "presets": { + "add": { + "button": "Adaugă la asistent", + "knowledge_base": { + "label": "Bază de cunoștințe", + "placeholder": "Selectează baza de cunoștințe" + }, + "name": { + "label": "Nume", + "placeholder": "Introdu numele" + }, + "prompt": { + "label": "Prompt", + "placeholder": "Introdu promptul", + "variables": { + "tip": { + "content": "{{date}}:\tDată\n{{time}}:\tOră\n{{datetime}}:\tDată și oră\n{{system}}:\tSistem de operare\n{{arch}}:\tArhitectură CPU\n{{language}}:\tLimbă\n{{model_name}}:\tNume model\n{{username}}:\tNume utilizator", + "title": "Variabile disponibile" + } + } + }, + "title": "Creează asistent", + "unsaved_changes_warning": "Ai modificări nesalvate. Ești sigur că vrei să închizi?" + }, + "delete": { + "popup": { + "content": "Ești sigur că vrei să ștergi acest asistent?" + } + }, + "edit": { + "model": { + "select": { + "title": "Selectează modelul" + } + }, + "title": "Editează asistentul" + }, + "export": { + "agent": "Exportă asistentul" + }, + "import": { + "button": "Importă", + "error": { + "fetch_failed": "Nu s-a putut prelua de la URL", + "file_required": "Te rugăm să selectezi mai întâi un fișier", + "invalid_format": "Format asistent invalid: lipsesc câmpuri obligatorii", + "url_required": "Te rugăm să introduci un URL" + }, + "file_filter": "Fișiere JSON", + "select_file": "Selectează fișierul", + "title": "Importă din exterior", + "type": { + "file": "Fișier", + "url": "URL" + }, + "url_placeholder": "Introdu URL JSON" + }, + "manage": { + "batch_delete": { + "button": "Șterge", + "confirm": "Ești sigur că vrei să ștergi cei {{count}} asistenți selectați?" + }, + "batch_export": { + "button": "Exportă" + }, + "mode": { + "manage": "Gestionează", + "sort": "Sortează" + }, + "title": "Gestionează asistenții" + }, + "my_agents": "Asistenții mei", + "search": { + "no_results": "Niciun rezultat găsit" + }, + "settings": { + "title": "Setare asistent" + }, + "sorting": { + "title": "Sortare" + }, + "tag": { + "agent": "Asistent", + "default": "Implicit", + "new": "Nou", + "system": "Sistem" + }, + "title": "Bibliotecă de asistenți" + }, + "save": { + "success": "Salvat cu succes", + "title": "Salvează în biblioteca de asistenți" + }, + "search": "Caută asistenți...", + "settings": { + "default_model": "Model implicit", + "knowledge_base": { + "label": "Setări bază de cunoștințe", + "recognition": { + "label": "Folosește baza de cunoștințe", + "off": "Forțează căutarea", + "on": "Recunoaștere intenție", + "tip": "Asistentul va folosi capacitatea de recunoaștere a intenției modelului mare pentru a determina dacă să folosească baza de cunoștințe pentru a răspunde. Această funcție depinde de capacitățile modelului" + } + }, + "mcp": { + "description": "Servere MCP activate implicit", + "enableFirst": "Activează mai întâi acest server în setările MCP", + "label": "Servere MCP", + "noServersAvailable": "Nu există servere MCP disponibile. Adaugă servere în setări", + "title": "Setări MCP" + }, + "model": "Setări model", + "more": "Setări asistent", + "prompt": "Setări prompt", + "reasoning_effort": { + "auto": "Auto", + "auto_description": "Determină flexibil efortul de raționament", + "default": "Implicit", + "default_description": "Depinde de comportamentul implicit al modelului, fără nicio configurare.", + "high": "Ridicat", + "high_description": "Raționament de nivel ridicat", + "label": "Efort de raționament", + "low": "Scăzut", + "low_description": "Raționament de nivel scăzut", + "medium": "Mediu", + "medium_description": "Raționament de nivel mediu", + "minimal": "Minim", + "minimal_description": "Raționament minim", + "off": "Oprit", + "off_description": "Dezactivează raționamentul", + "xhigh": "Extra ridicat", + "xhigh_description": "Raționament de nivel extra ridicat" + }, + "regular_phrases": { + "add": "Adaugă expresie", + "contentLabel": "Conținut", + "contentPlaceholder": "Te rugăm să introduci conținutul expresiei; poți folosi variabile și poți apăsa Tab pentru a localiza rapid variabila de modificat. De exemplu: \nAjută-mă să planific o rută de la ${from} până la ${to} și trimite-o la ${email}.", + "delete": "Șterge expresia", + "deleteConfirm": "Ești sigur că vrei să ștergi această expresie?", + "edit": "Editează expresia", + "title": "Expresie uzuală", + "titleLabel": "Titlu", + "titlePlaceholder": "Introdu titlul" + }, + "title": "Setări asistent", + "tool_use_mode": { + "function": "Funcție", + "label": "Mod utilizare instrumente", + "prompt": "Prompt" + } + }, + "tags": { + "add": "Adaugă etichetă", + "delete": "Șterge eticheta", + "deleteConfirm": "Ești sigur că vrei să ștergi această etichetă?", + "manage": "Gestionare etichete", + "modify": "Modifică eticheta", + "none": "Fără etichete", + "settings": { + "title": "Setări etichete" + }, + "untagged": "Neetichetat" + }, + "title": "Asistenți" + }, + "auth": { + "error": "Obținerea automată a cheii API a eșuat, te rugăm să o obții manual", + "get_key": "Obține", + "get_key_success": "Cheia API a fost obținută automat cu succes", + "login": "Autentificare", + "oauth_button": "Autentificare cu {{provider}}" + }, + "backup": { + "confirm": { + "button": "Selectează locația de backup", + "label": "Ești sigur că vrei să faci backup la date?" + }, + "content": "Se face backup la toate datele, inclusiv istoricul chat-ului, setările și baza de cunoștințe. Te rugăm să reții că procesul de backup poate dura ceva timp, îți mulțumim pentru răbdare.", + "progress": { + "completed": "Backup finalizat", + "compressing": "Se comprimă fișierele...", + "copying_files": "Se copiază fișierele... {{progress}}%", + "preparing": "Se pregătește backup-ul...", + "preparing_compression": "Se pregătește compresia...", + "title": "Progres backup", + "writing_data": "Se scriu datele..." + }, + "title": "Backup date" + }, + "button": { + "add": "Adaugă", + "added": "Adăugat", + "case_sensitive": "Sensibil la majuscule", + "collapse": "Restrânge", + "download": "Descarcă", + "includes_user_questions": "Include întrebările tale", + "manage": "Gestionează", + "select_model": "Selectează modelul", + "show": { + "all": "Arată tot" + }, + "update_available": "Actualizare disponibilă", + "whole_word": "Cuvânt întreg" + }, + "chat": { + "add": { + "assistant": { + "description": "Conversații zilnice și întrebări rapide", + "title": "Adaugă asistent" + }, + "option": { + "title": "Selectează tipul" + }, + "topic": { + "title": "Subiect nou" + } + }, + "artifacts": { + "button": { + "download": "Descarcă", + "openExternal": "Deschide în browser extern", + "preview": "Previzualizare" + }, + "preview": { + "openExternal": { + "error": { + "content": "Eroare la deschiderea browserului extern." + } + } + } + }, + "assistant": { + "search": { + "placeholder": "Caută" + } + }, + "deeply_thought": "Gândit profund ({{seconds}} secunde)", + "default": { + "description": "Salut, sunt Asistentul Implicit. Poți începe să discuți cu mine imediat", + "name": "Asistent Implicit", + "topic": { + "name": "Subiect implicit" + } + }, + "history": { + "assistant_node": "Asistent", + "click_to_navigate": "Fă clic pentru a naviga la mesaj", + "coming_soon": "Diagrama fluxului de chat va fi disponibilă în curând", + "no_messages": "Nu au fost găsite mesaje", + "start_conversation": "Începe o conversație pentru a vedea diagrama fluxului de chat", + "title": "Istoric chat", + "user_node": "Utilizator", + "view_full_content": "Vezi conținutul complet" + }, + "input": { + "activity_directory": { + "description": "Selectează fișierul din directorul de activitate", + "loading": "Se încarcă fișierele...", + "no_file_found": { + "description": "Nu există fișiere disponibile în directoarele accesibile", + "label": "Nu a fost găsit niciun fișier" + }, + "title": "Director de activitate" + }, + "auto_resize": "Redimensionare automată înălțime", + "clear": { + "content": "Vrei să ștergi toate mesajele subiectului curent?", + "label": "Șterge {{Command}}", + "title": "Ștergi toate mesajele?" + }, + "collapse": "Restrânge", + "context_count": { + "tip": "Context / Context maxim" + }, + "estimated_tokens": { + "tip": "Tokeni estimați" + }, + "expand": "Extinde", + "file_error": "Eroare la procesarea fișierului", + "file_not_supported": "Modelul nu acceptă acest tip de fișier", + "file_not_supported_count": "{{count}} fișiere nu sunt acceptate", + "generate_image": "Generează imagine", + "generate_image_not_supported": "Modelul nu acceptă generarea de imagini.", + "knowledge_base": "Bază de cunoștințe", + "new": { + "context": "Șterge contextul {{Command}}" + }, + "new_session": "Sesiune nouă {{Command}}", + "new_topic": "Subiect nou {{Command}}", + "paste_text_file_confirm": "Lipești în bara de introducere?", + "pause": "Pauză", + "placeholder": "Scrie mesajul tău aici, apasă {{key}} pentru a trimite - @ pentru a selecta modelul, / pentru a include instrumente", + "placeholder_without_triggers": "Scrie mesajul tău aici, apasă {{key}} pentru a trimite", + "send": "Trimite", + "settings": "Setări", + "slash_commands": { + "description": "Comenzi slash pentru sesiunea agentului", + "title": "Comenzi slash" + }, + "thinking": { + "budget_exceeds_max": "Bugetul de gândire depășește numărul maxim de tokeni", + "label": "Gândire", + "mode": { + "custom": { + "label": "Personalizat", + "tip": "Numărul maxim de tokeni pe care modelul îi poate gândi. Trebuie să iei în considerare limita de context a modelului, altfel va fi raportată o eroare" + }, + "default": { + "label": "Implicit", + "tip": "Modelul va determina automat numărul de tokeni pentru gândire" + }, + "tokens": { + "tip": "Setează numărul de tokeni de gândire de utilizat." + } + } + }, + "tools": { + "collapse": "Restrânge", + "collapse_in": "Restrânge", + "collapse_out": "Elimină din restrângere", + "expand": "Extinde" + }, + "topics": " Subiecte ", + "translate": "Tradu în {{target_language}}", + "translating": "Se traduce...", + "upload": { + "attachment": "Încarcă atașament", + "document": "Încarcă fișier document (modelul nu acceptă imagini)", + "image_or_document": "Încarcă imagine sau fișier document", + "upload_from_local": "Încarcă fișier local..." + }, + "url_context": "Context URL", + "web_search": { + "builtin": { + "disabled_content": "Modelul curent nu acceptă căutarea web", + "enabled_content": "Folosește funcția de căutare web integrată a modelului", + "label": "Integrat în model" + }, + "button": { + "ok": "Mergi la Setări" + }, + "enable": "Activează căutarea web", + "enable_content": "Trebuie să verifici mai întâi conectivitatea căutării web în setări", + "label": "Căutare web", + "no_web_search": { + "description": "Nu activa căutarea web", + "label": "Dezactivează căutarea web" + }, + "settings": "Setări căutare web" + } + }, + "mcp": { + "error": { + "parse_tool_call": "Nu se poate converti într-un format valid de apelare a instrumentului: {{toolCall}}" + }, + "warning": { + "gemini_web_search": "Gemini nu acceptă utilizarea simultană a instrumentelor native de căutare web și a apelării funcțiilor", + "multiple_tools": "Există mai multe instrumente MCP care se potrivesc, a fost selectat {{tool}}", + "no_tool": "Nu s-a găsit niciun instrument MCP potrivit pentru {{tool}}", + "url_context": "Gemini nu acceptă utilizarea simultană a contextului URL și a apelării funcțiilor" + } + }, + "message": { + "new": { + "branch": { + "created": "Ramură nouă creată", + "label": "Ramură nouă" + }, + "context": "Context nou" + }, + "quote": "Citează", + "regenerate": { + "model": "Schimbă modelul" + }, + "useful": { + "label": "Setează ca context", + "tip": "În acest grup de mesaje, acest mesaj va fi selectat pentru a se alătura contextului" + } + }, + "multiple": { + "select": { + "empty": "Niciun mesaj selectat", + "label": "Selecție multiplă" + } + }, + "navigation": { + "bottom": "Înapoi jos", + "close": "Închide", + "first": "Deja la primul mesaj", + "history": "Istoric chat", + "last": "Deja la ultimul mesaj", + "next": "Mesajul următor", + "prev": "Mesajul anterior", + "top": "Înapoi sus" + }, + "resend": "Retrimite", + "save": { + "file": { + "title": "Salvează în fișier local" + }, + "knowledge": { + "content": { + "citation": { + "description": "Include informații de referință din căutarea web și baza de cunoștințe", + "title": "Citări" + }, + "code": { + "description": "Include blocuri de cod independente", + "title": "Blocuri de cod" + }, + "error": { + "description": "Include mesaje de eroare din timpul execuției", + "title": "Erori" + }, + "file": { + "description": "Include fișierele atașate", + "title": "Fișiere" + }, + "maintext": { + "description": "Include conținutul textului principal", + "title": "Text principal" + }, + "thinking": { + "description": "Include conținutul raționamentului modelului", + "title": "Raționament" + }, + "tool_use": { + "description": "Include parametrii apelului instrumentului și rezultatele execuției", + "title": "Utilizare instrument" + }, + "translation": { + "description": "Include conținutul traducerii", + "title": "Traduceri" + } + }, + "empty": { + "no_content": "Acest mesaj nu are conținut care poate fi salvat", + "no_knowledge_base": "Nicio bază de cunoștințe disponibilă, te rugăm să creezi una mai întâi" + }, + "error": { + "invalid_base": "Baza de cunoștințe selectată nu este configurată corect", + "no_content_selected": "Te rugăm să selectezi cel puțin un tip de conținut", + "save_failed": "Salvarea a eșuat, te rugăm să verifici configurația bazei de cunoștințe" + }, + "select": { + "base": { + "placeholder": "Te rugăm să selectezi o bază de cunoștințe", + "title": "Selectează baza de cunoștințe" + }, + "content": { + "tip": "S-au selectat {{count}} elemente, tipurile de text vor fi îmbinate și salvate ca o singură notiță", + "title": "Selectează tipurile de conținut pentru salvare" + } + }, + "title": "Salvează în Baza de cunoștințe" + }, + "label": "Salvează", + "topic": { + "knowledge": { + "content": { + "maintext": { + "description": "Include titlul subiectului și conținutul textului principal din toate mesajele" + } + }, + "empty": { + "no_content": "Acest subiect nu are conținut care poate fi salvat" + }, + "error": { + "save_failed": "Nu s-a putut salva subiectul, te rugăm să verifici configurația bazei de cunoștințe" + }, + "loading": "Se analizează conținutul subiectului...", + "select": { + "content": { + "label": "Selectează tipurile de conținut pentru salvare", + "selected_tip": "S-au selectat {{count}} elemente din {{messages}} mesaje", + "tip": "Subiectul va fi salvat în baza de cunoștințe cu contextul complet al conversației" + } + }, + "success": "Subiect salvat cu succes în baza de cunoștințe ({{count}} elemente)", + "title": "Salvează subiectul în Baza de cunoștințe" + } + } + }, + "settings": { + "code": { + "title": "Setări blocuri de cod" + }, + "code_collapsible": "Bloc de cod restrâns", + "code_editor": { + "autocompletion": "Completare automată", + "fold_gutter": "Zonă de pliere", + "highlight_active_line": "Evidențiază linia activă", + "keymap": "Mapare taste", + "title": "Editor de cod" + }, + "code_execution": { + "timeout_minutes": { + "label": "Expirare", + "tip": "Timpul de expirare (minute) al execuției codului" + }, + "tip": "Butonul de rulare va fi afișat în bara de instrumente a blocurilor de cod executabile; te rugăm să nu execuți cod periculos!", + "title": "Execuție cod" + }, + "code_fancy_block": { + "label": "Bloc de cod stilizat", + "tip": "Activează stilul sofisticat pentru blocul de cod, de ex., card html" + }, + "code_image_tools": { + "label": "Activează instrumentele de previzualizare", + "tip": "Activează instrumentele de previzualizare pentru imaginile randate din blocuri de cod, cum ar fi mermaid" + }, + "code_wrappable": "Încadrare text în blocul de cod", + "context_count": { + "label": "Context", + "tip": "Numărul de mesaje anterioare de păstrat în context." + }, + "max": "Nelimitat", + "max_tokens": { + "confirm": "Setează tokeni maximi", + "confirm_content": "Setează numărul maxim de tokeni pe care modelul îi poate genera. Trebuie să iei în considerare limita de context a modelului, altfel va fi raportată o eroare", + "label": "Setează tokeni maximi", + "tip": "Numărul maxim de tokeni pe care modelul îi poate genera. Trebuie să iei în considerare limita de context a modelului, altfel va fi raportată o eroare" + }, + "reset": "Resetează", + "set_as_default": "Aplică la asistentul implicit", + "show_line_numbers": "Arată numerele de linie în cod", + "temperature": { + "label": "Temperatură", + "tip": "Valorile mai mari fac modelul mai creativ și imprevizibil, în timp ce valorile mai mici îl fac mai determinist și precis." + }, + "thought_auto_collapse": { + "label": "Restrânge conținutul gândirii", + "tip": "Restrânge automat conținutul gândirii după ce gândirea se termină" + }, + "top_p": { + "label": "Top-P", + "tip": "Valoarea implicită este 1; cu cât valoarea este mai mică, cu atât mai puțină varietate în răspunsuri și mai ușor de înțeles; cu cât valoarea este mai mare, cu atât gama de vocabular a AI-ului este mai largă și mai diversă" + } + }, + "suggestions": { + "title": "Întrebări sugerate" + }, + "thinking": "Gândire ({{seconds}} secunde)", + "topics": { + "auto_rename": "Redenumire automată", + "clear": { + "title": "Șterge mesajele" + }, + "copy": { + "image": "Copiază ca imagine", + "md": "Copiază ca markdown", + "plain_text": "Copiază ca text simplu (elimină Markdown)", + "title": "Copiază" + }, + "delete": { + "shortcut": "Ține apăsat {{key}} pentru a șterge direct" + }, + "edit": { + "placeholder": "Introdu noul nume", + "title": "Editează numele", + "title_tip": "Sfat: Fă dublu clic pe numele subiectului pentru a-l redenumi direct" + }, + "export": { + "image": "Exportă ca imagine", + "joplin": "Exportă în Joplin", + "md": { + "label": "Exportă ca markdown", + "reason": "Exportă ca Markdown (cu raționament)" + }, + "notes": "Exportă în Note", + "notion": "Exportă în Notion", + "obsidian": "Exportă în Obsidian", + "obsidian_atributes": "Configurează atributele notiței", + "obsidian_btn": "Confirmă", + "obsidian_created": "Ora creării", + "obsidian_created_placeholder": "Te rugăm să selectezi ora creării", + "obsidian_export_failed": "Exportul a eșuat", + "obsidian_export_success": "Export reușit", + "obsidian_fetch_error": "Nu s-au putut prelua seifurile Obsidian", + "obsidian_fetch_folders_error": "Nu s-a putut prelua structura folderelor", + "obsidian_loading": "Se încarcă...", + "obsidian_no_vault_selected": "Te rugăm să selectezi mai întâi un seif", + "obsidian_no_vaults": "Nu s-au găsit seifuri Obsidian", + "obsidian_operate": "Metodă de operare", + "obsidian_operate_append": "Adaugă la sfârșit", + "obsidian_operate_new_or_overwrite": "Creează nou (Suprascrie dacă există)", + "obsidian_operate_placeholder": "Te rugăm să selectezi metoda de operare", + "obsidian_operate_prepend": "Adaugă la început", + "obsidian_path": "Cale", + "obsidian_path_placeholder": "Te rugăm să selectezi calea", + "obsidian_reasoning": "Include lanțul de raționament", + "obsidian_root_directory": "Director rădăcină", + "obsidian_select_vault_first": "Te rugăm să selectezi mai întâi un seif", + "obsidian_source": "Sursă", + "obsidian_source_placeholder": "Te rugăm să introduci sursa", + "obsidian_tags": "Etichete", + "obsidian_tags_placeholder": "Te rugăm să introduci etichete, separă etichetele multiple prin virgule", + "obsidian_title": "Titlu", + "obsidian_title_placeholder": "Te rugăm să introduci titlul", + "obsidian_title_required": "Titlul nu poate fi gol", + "obsidian_vault": "Seif", + "obsidian_vault_placeholder": "Te rugăm să selectezi numele seifului", + "siyuan": "Exportă în Siyuan Note", + "title": "Exportă", + "title_naming_failed": "Nu s-a putut genera titlul, se folosește titlul implicit", + "title_naming_success": "Titlu generat cu succes", + "wait_for_title_naming": "Se generează titlul...", + "word": "Exportă ca Word", + "yuque": "Exportă în Yuque" + }, + "list": "Listă subiecte", + "manage": { + "clear_selection": "Șterge selecția", + "delete": { + "confirm": { + "content": "Ești sigur că vrei să ștergi {{count}} subiecte selectate? Această acțiune nu poate fi anulată.", + "title": "Șterge subiecte" + }, + "success": "S-au șters {{count}} subiecte" + }, + "deselect_all": "Deselectează tot", + "error": { + "at_least_one": "Trebuie păstrat cel puțin un subiect" + }, + "move": { + "button": "Mută", + "placeholder": "Selectează asistentul țintă", + "success": "S-au mutat {{count}} subiecte" + }, + "pinned": "Subiecte fixate", + "selected_count": "{{count}} selectate", + "title": "Gestionează subiectele", + "unpinned": "Subiecte nefixate" + }, + "move_to": "Mută la", + "new": "Subiect nou", + "pin": "Fixează subiectul", + "prompt": { + "edit": { + "title": "Editează prompturile subiectului" + }, + "label": "Prompturi subiect", + "tips": "Prompturi subiect: Prompturi suplimentare furnizate pentru subiectul curent" + }, + "search": { + "placeholder": "Caută subiecte...", + "title": "Caută" + }, + "title": "Subiecte", + "unpin": "Detașează subiectul" + }, + "translate": "Tradu", + "web_search": { + "warning": { + "openai": "Efortul minim de raționament al modelului GPT-5 nu acceptă căutarea web." + } + } + }, + "code": { + "auto_update_to_latest": "Actualizează automat la cea mai recentă versiune", + "bun_required_message": "Mediul Bun este necesar pentru a rula instrumente CLI", + "cli_tool": "Instrument CLI", + "cli_tool_placeholder": "Selectează instrumentul CLI de utilizat", + "custom_path": "Cale personalizată", + "custom_path_error": "Nu s-a putut seta calea personalizată a terminalului", + "custom_path_required": "Calea personalizată este necesară pentru acest terminal", + "custom_path_set": "Calea personalizată a terminalului a fost setată cu succes", + "description": "Lansează rapid mai multe instrumente CLI de cod pentru a îmbunătăți eficiența dezvoltării", + "env_vars_help": "Introdu variabile de mediu personalizate (una pe rând, format: CHEIE=valoare)", + "environment_variables": "Variabile de mediu", + "folder_placeholder": "Selectează directorul de lucru", + "install_bun": "Instalează Bun", + "installing_bun": "Se instalează...", + "launch": { + "bun_required": "Te rugăm să instalezi mai întâi mediul Bun înainte de a lansa instrumentele CLI", + "error": "Lansarea a eșuat, te rugăm să încerci din nou", + "label": "Lansează", + "success": "Lansare reușită", + "validation_error": "Te rugăm să completezi toate câmpurile obligatorii: instrument CLI, model și director de lucru" + }, + "launching": "Se lansează...", + "model": "Model", + "model_placeholder": "Selectează modelul de utilizat", + "model_required": "Te rugăm să selectezi un model", + "select_folder": "Selectează folderul", + "set_custom_path": "Setează calea personalizată a terminalului", + "supported_providers": "Furnizori acceptați", + "terminal": "Terminal", + "terminal_placeholder": "Selectează aplicația terminal", + "title": "Instrumente de cod", + "update_options": "Opțiuni de actualizare", + "working_directory": "Director de lucru" + }, + "code_block": { + "collapse": "Restrânge", + "copy": { + "failed": "Copiere eșuată", + "label": "Copiază", + "source": "Copiază codul sursă", + "success": "Copiat" + }, + "download": { + "failed": { + "network": "Descărcarea a eșuat, te rugăm să verifici rețeaua" + }, + "label": "Descarcă", + "png": "Descarcă PNG", + "source": "Descarcă codul sursă", + "svg": "Descarcă SVG" + }, + "edit": { + "label": "Editează", + "save": { + "failed": { + "label": "Salvare eșuată", + "message_not_found": "Salvare eșuată, mesajul nu a fost găsit" + }, + "label": "Salvează modificările", + "success": "Salvat" + } + }, + "expand": "Extinde", + "more": "Mai mult", + "run": "Rulează", + "split": { + "label": "Vizualizare divizată", + "restore": "Restaurează vizualizarea divizată" + }, + "wrap": { + "off": "Nu încadra", + "on": "Încadrează" + } + }, + "common": { + "about": "Despre", + "add": "Adaugă", + "add_success": "Adăugat cu succes", + "advanced_settings": "Setări avansate", + "agent_one": "Agent", + "agent_other": "Agenți", + "and": "și", + "assistant": "Agent", + "assistant_one": "Asistent", + "assistant_other": "Asistenți", + "avatar": "Avatar", + "back": "Înapoi", + "browse": "Răsfoiește", + "cancel": "Anulează", + "chat": "Chat", + "clear": "Golește", + "close": "Închide", + "collapse": "Restrânge", + "completed": "Finalizat", + "confirm": "Confirmă", + "copied": "Copiat", + "copy": "Copiază", + "copy_failed": "Copiere eșuată", + "current": "Curent", + "cut": "Taie", + "default": "Implicit", + "delete": "Șterge", + "delete_confirm": "Ești sigur că vrei să ștergi?", + "delete_failed": "Nu s-a putut șterge", + "delete_success": "Șters cu succes", + "description": "Descriere", + "detail": "Detaliu", + "disabled": "Dezactivat", + "docs": "Documentație", + "download": "Descarcă", + "duplicate": "Duplică", + "edit": "Editează", + "enabled": "Activat", + "error": "eroare", + "errors": { + "create_message": "Nu s-a putut crea mesajul", + "validation": "Verificarea a eșuat" + }, + "expand": "Extinde", + "file": { + "not_supported": "Tip de fișier neacceptat {{type}}" + }, + "footnote": "Conținut de referință", + "footnotes": "Referințe", + "fullscreen": "S-a intrat în modul ecran complet. Apasă F11 pentru a ieși", + "go_to_settings": "Mergi la setări", + "i_know": "Am înțeles", + "ignore": "Ignoră", + "inspect": "Inspectează", + "invalid_value": "Valoare invalidă", + "knowledge_base": "Bază de cunoștințe", + "language": "Limbă", + "loading": "Se încarcă...", + "model": "Model", + "models": "Modele", + "more": "Mai mult", + "name": "Nume", + "no_results": "Niciun rezultat", + "none": "Nimic", + "off": "Oprit", + "on": "Pornit", + "open": "Deschide", + "paste": "Lipește", + "placeholders": { + "select": { + "model": "Selectează un model" + } + }, + "preview": "Previzualizare", + "prompt": "Prompt", + "provider": "Furnizor", + "reasoning_content": "Raționament profund", + "refresh": "Reîmprospătează", + "regenerate": "Regenerează", + "rename": "Redenumește", + "reset": "Resetează", + "save": "Salvează", + "saved": "Salvat", + "search": "Caută", + "select": "Selectează", + "select_all": "Selectează tot", + "selected": "Selectat", + "selectedItems": "{{count}} elemente selectate", + "selectedMessages": "{{count}} mesaje selectate", + "settings": "Setări", + "sort": { + "pinyin": { + "asc": "Sortează după Pinyin (A-Z)", + "desc": "Sortează după Pinyin (Z-A)", + "label": "Sortează după Pinyin" + } + }, + "stop": "Oprește", + "subscribe": "Abonează-te", + "success": "Succes", + "swap": "Schimbă", + "topics": "Subiecte", + "unknown": "Necunoscut", + "unnamed": "Fără nume", + "unsubscribe": "Dezabonează-te", + "update_success": "Actualizat cu succes", + "upload_files": "Încarcă fișier", + "warning": "Avertisment", + "you": "Tu" + }, + "docs": { + "title": "Documentație" + }, + "endpoint_type": { + "anthropic": "Anthropic", + "gemini": "Gemini", + "image-generation": "Generare imagini (OpenAI)", + "jina-rerank": "Jina Rerank", + "openai": "OpenAI", + "openai-response": "OpenAI-Response" + }, + "error": { + "availableProviders": "Furnizori disponibili", + "availableTools": "Instrumente disponibile", + "backup": { + "file_format": "Eroare format fișier backup" + }, + "boundary": { + "default": { + "devtools": "Deschide panoul de depanare", + "message": "Se pare că ceva nu a mers bine...", + "reload": "Reîncarcă" + }, + "details": "Detalii", + "mcp": { + "invalid": "Server MCP invalid" + } + }, + "cause": "Cauză eroare", + "chat": { + "chunk": { + "non_json": "S-a returnat un format de date invalid" + }, + "insufficient_balance": "Te rugăm să mergi la {{provider}} pentru a reîncărca.", + "no_api_key": "Nu ai configurat o cheie API. Te rugăm să mergi la {{provider}} pentru a obține o cheie API.", + "quota_exceeded": "Cota ta gratuită zilnică {{quota}} a fost epuizată. Te rugăm să mergi la {{provider}} pentru a obține o cheie API și configurează cheia API pentru a continua utilizarea.", + "response": "Ceva nu a mers bine. Te rugăm să verifici dacă ai setat cheia API în Setări > Furnizori" + }, + "content": "Conținut", + "data": "Date", + "detail": "Detalii eroare", + "details": "Detalii", + "errors": "Erori", + "finishReason": "Motiv finalizare", + "functionality": "Funcționalitate", + "http": { + "400": "Cererea a eșuat. Te rugăm să verifici dacă parametrii cererii sunt corecți. Dacă ai modificat setările modelului, te rugăm să le resetezi la valorile implicite", + "401": "Autentificarea a eșuat. Te rugăm să verifici dacă cheia API este corectă", + "403": "Acces refuzat. Te rugăm să verifici dacă contul tău este verificat sau contactează furnizorul de servicii pentru mai multe informații", + "404": "Modelul nu a fost găsit sau calea cererii este incorectă", + "429": "Prea multe cereri. Te rugăm să încerci din nou mai târziu", + "500": "Eroare de server. Te rugăm să încerci din nou mai târziu", + "502": "Eroare gateway. Te rugăm să încerci din nou mai târziu", + "503": "Serviciu indisponibil. Te rugăm să încerci din nou mai târziu", + "504": "Expirare gateway. Te rugăm să încerci din nou mai târziu" + }, + "lastError": "Ultima eroare", + "maxEmbeddingsPerCall": "Max Embeddings per apel", + "message": "Mesaj de eroare", + "missing_user_message": "Nu se poate schimba răspunsul modelului: Mesajul original al utilizatorului a fost șters. Te rugăm să trimiți un mesaj nou pentru a primi un răspuns cu acest model.", + "model": { + "exists": "Modelul există deja", + "not_exists": "Modelul nu există" + }, + "modelId": "ID model", + "modelType": "Tip model", + "name": "Nume eroare", + "no_api_key": "Cheia API nu este configurată", + "no_response": "Niciun răspuns", + "originalError": "Eroare originală", + "originalMessage": "Mesaj original", + "parameter": "Parametru", + "pause_placeholder": "În pauză", + "prompt": "Prompt", + "provider": "Furnizor", + "providerId": "ID furnizor", + "provider_disabled": "Furnizorul modelului nu este activat", + "reason": "Motiv", + "render": { + "description": "Nu s-a putut randa conținutul mesajului. Te rugăm să verifici dacă formatul conținutului mesajului este corect", + "title": "Eroare de randare" + }, + "requestBody": "Corp cerere", + "requestBodyValues": "Valori corp cerere", + "requestUrl": "URL cerere", + "response": "Răspuns", + "responseBody": "Corp răspuns", + "responseHeaders": "Header răspuns", + "responses": "Răspunsuri", + "role": "Rol", + "stack": "Stack Trace", + "status": "Cod stare", + "statusCode": "Cod stare", + "statusText": "Text stare", + "text": "Text", + "toolInput": "Intrare instrument", + "toolName": "Nume instrument", + "unknown": "Eroare necunoscută", + "usage": "Utilizare", + "user_message_not_found": "Nu se poate găsi mesajul original al utilizatorului pentru a retrimite", + "value": "Valoare", + "values": "Valori" + }, + "export": { + "assistant": "Asistent", + "attached_files": "Fișiere atașate", + "conversation_details": "Detalii conversație", + "conversation_history": "Istoric conversație", + "created": "Creat", + "last_updated": "Ultima actualizare", + "messages": "Mesaje", + "notion": { + "reasoning_truncated": "Lanțul de gândire nu poate fi fragmentat și a fost trunchiat." + }, + "user": "Utilizator" + }, + "files": { + "actions": "Acțiuni", + "all": "Toate fișierele", + "batch_delete": "Ștergere în lot", + "batch_operation": "Selectează tot", + "count": "fișiere", + "created_at": "Creat la", + "delete": { + "content": "Ștergerea unui fișier va șterge referința acestuia din toate mesajele. Ești sigur că vrei să ștergi acest fișier?", + "db_error": "Ștergerea a eșuat", + "label": "Șterge", + "paintings": { + "warning": "Imaginea conține acest fișier, ștergerea nu este posibilă" + }, + "title": "Șterge fișier" + }, + "document": "Document", + "edit": "Editează", + "error": { + "open_path": "Nu s-a putut deschide calea {{path}}" + }, + "file": "Fișier", + "image": "Imagine", + "name": "Nume", + "open": "Deschide", + "preview": { + "error": "Nu s-a putut deschide fișierul" + }, + "size": "Dimensiune", + "text": "Text", + "title": "Fișiere", + "type": "Tip" + }, + "gpustack": { + "keep_alive_time": { + "description": "Timpul în minute pentru a menține conexiunea activă; implicit este 5 minute.", + "placeholder": "Minute", + "title": "Timp menținere conexiune" + }, + "title": "GPUStack" + }, + "history": { + "continue_chat": "Continuă conversația", + "error": { + "topic_not_found": "Subiectul nu a fost găsit" + }, + "locate": { + "message": "Localizează mesajul" + }, + "search": { + "messages": "Caută în toate mesajele", + "placeholder": "Caută subiecte sau mesaje...", + "topics": { + "empty": "Nu s-au găsit subiecte, apasă Enter pentru a căuta în toate mesajele" + } + }, + "title": "Căutare subiecte" + }, + "html_artifacts": { + "capture": { + "label": "Capturează pagina", + "to_clipboard": "Copiază în clipboard", + "to_file": "Salvează ca imagine" + }, + "code": "Cod", + "empty_preview": "Niciun conținut de afișat", + "generating": "Se generează", + "preview": "Previzualizare", + "split": "Divizat" + }, + "import": { + "chatgpt": { + "assistant_name": "Import ChatGPT", + "button": "Selectează fișierul", + "description": "Importă doar textul conversației, nu include imagini și atașamente", + "error": { + "invalid_json": "Format fișier JSON invalid", + "no_conversations": "Nu s-au găsit conversații în fișier", + "no_valid_conversations": "Nu există conversații valide de importat", + "unknown": "Importul a eșuat, te rugăm să verifici formatul fișierului" + }, + "help": { + "step1": "1. Conectează-te la ChatGPT, mergi la Settings > Data controls > Export data", + "step2": "2. Așteaptă fișierul de export pe e-mail", + "step3": "3. Extrage arhiva descărcată și găsește conversations.json", + "title": "Cum export conversațiile ChatGPT?" + }, + "importing": "Se importă conversațiile...", + "selecting": "Se selectează fișierul...", + "success": "S-au importat cu succes {{topics}} conversații cu {{messages}} mesaje", + "title": "Importă conversații ChatGPT", + "untitled_conversation": "Conversație fără titlu" + }, + "confirm": { + "button": "Selectează fișierul de import", + "label": "Ești sigur că vrei să imporți date externe?" + }, + "content": "Selectează fișierul de conversație din aplicația externă pentru import; momentan acceptă doar fișiere în format JSON ChatGPT", + "title": "Importă conversații externe" + }, + "knowledge": { + "add": { + "title": "Adaugă bază de cunoștințe" + }, + "add_directory": "Adaugă director", + "add_file": "Adaugă fișier", + "add_image": "Adaugă imagine", + "add_note": "Adaugă notă", + "add_sitemap": "Hartă site", + "add_url": "Adaugă URL", + "add_video": "Adaugă video", + "cancel_index": "Anulează indexarea", + "chunk_overlap": "Suprapunere fragmente", + "chunk_overlap_placeholder": "Implicit (nu se recomandă modificarea)", + "chunk_overlap_tooltip": "Cantitatea de conținut duplicat între fragmentele adiacente, asigurând că fragmentele sunt încă legate contextual, îmbunătățind efectul general al procesării textului lung", + "chunk_size": "Dimensiune fragment", + "chunk_size_change_warning": "Modificările dimensiunii fragmentului și ale suprapunerii se aplică doar conținutului nou", + "chunk_size_placeholder": "Implicit (nu se recomandă modificarea)", + "chunk_size_too_large": "Dimensiunea fragmentului nu poate depăși limita de context a modelului ({{max_context}})", + "chunk_size_tooltip": "Împarte documentele în fragmente; dimensiunea fiecărui fragment nu trebuie să depășească limita de context a modelului", + "clear_selection": "Șterge selecția", + "delete": "Șterge", + "delete_confirm": "Ești sigur că vrei să ștergi această bază de cunoștințe?", + "dimensions": "Dimensiune embedding", + "dimensions_auto_set": "Setează automat dimensiunile embedding", + "dimensions_default": "Modelul va folosi dimensiunile implicite de embedding", + "dimensions_error_invalid": "Dimensiune embedding invalidă", + "dimensions_set_right": "⚠️ Te rugăm să te asiguri că modelul acceptă dimensiunea embedding setată", + "dimensions_size_placeholder": "Lasă gol pentru a nu transmite dimensiuni", + "dimensions_size_too_large": "Dimensiunea embedding nu poate depăși limita de context a modelului ({{max_context}}).", + "dimensions_size_tooltip": "Dimensiunea embedding; cu cât valoarea este mai mare, cu atât se vor consuma mai mulți tokeni. Lasă gol pentru a nu transmite parametrul dimensions.", + "directories": "Directoare", + "directory_placeholder": "Introdu calea directorului", + "document_count": "Fragmente de document solicitate", + "document_count_default": "Implicit", + "document_count_help": "Cu cât sunt solicitate mai multe fragmente de document, cu atât sunt incluse mai multe informații, dar se consumă mai mulți tokeni", + "drag_file": "Trage fișierul aici", + "drag_image": "Trage imaginea aici", + "edit_remark": "Editează observația", + "edit_remark_placeholder": "Te rugăm să introduci conținutul observației", + "embedding_model": "Model embedding", + "embedding_model_required": "Modelul de embedding pentru baza de cunoștințe este necesar", + "empty": "Nu a fost găsită nicio bază de cunoștințe", + "error": { + "failed_to_create": "Crearea bazei de cunoștințe a eșuat", + "failed_to_edit": "Editarea bazei de cunoștințe a eșuat", + "model_invalid": "Niciun model selectat", + "video": { + "local_file_missing": "Fișierul video nu a fost găsit", + "youtube_url_missing": "URL-ul video YouTube nu a fost găsit" + } + }, + "file_hint": "Acceptă {{file_types}}", + "image_hint": "Acceptă {{image_types}}", + "images": "Imagini", + "index_all": "Indexează tot", + "index_cancelled": "Indexare anulată", + "index_started": "Indexare pornită", + "invalid_url": "URL invalid", + "migrate": { + "button": { + "text": "Migrează" + }, + "confirm": { + "content": "S-au detectat modificări în modelul sau dimensiunea de embedding; configurarea nu poate fi salvată direct. Migrarea bazei de cunoștințe nu va șterge baza existentă, ci va crea o copie și apoi va reprocesa toate intrările, ceea ce poate consuma un număr mare de tokeni. Te rugăm să procedezi cu precauție.", + "ok": "Începe migrarea", + "title": "Migrare bază de cunoștințe" + }, + "error": { + "failed": "Migrarea a eșuat" + }, + "source_dimensions": "Dimensiuni sursă", + "source_model": "Model sursă", + "target_dimensions": "Dimensiuni țintă", + "target_model": "Model țintă" + }, + "model_info": "Informații model", + "name_required": "Numele bazei de cunoștințe este obligatoriu", + "no_bases": "Nu există baze de cunoștințe disponibile", + "no_match": "Nu s-a găsit conținut potrivit în baza de cunoștințe.", + "no_provider": "Furnizorul modelului pentru baza de cunoștințe nu este setat, baza de cunoștințe nu va mai fi acceptată; te rugăm să creezi o nouă bază de cunoștințe", + "not_set": "Nesetat", + "not_support": "Motorul bazei de date de cunoștințe a fost actualizat, baza de cunoștințe nu va mai fi acceptată; te rugăm să creezi o nouă bază de cunoștințe", + "notes": "Note", + "notes_placeholder": "Introdu informații suplimentare sau context pentru această bază de cunoștințe...", + "provider_not_found": "Furnizorul nu a fost găsit", + "quota": "Cotă rămasă {{name}}: {{quota}}", + "quota_empty": "Cota de astăzi pentru {{name}} este epuizată, te rugăm să aplici pe site-ul oficial", + "quota_infinity": "Cotă {{name}}: Nelimitat", + "rename": "Redenumește", + "search": "Caută în baza de cunoștințe", + "search_placeholder": "Introdu text pentru căutare", + "settings": { + "preprocessing": "Preprocesare", + "preprocessing_tooltip": "Preprocesează fișierele încărcate", + "title": "Setări bază de cunoștințe" + }, + "sitemap_added": "Adăugat cu succes", + "sitemap_placeholder": "Introdu URL-ul hărții site-ului", + "sitemaps": "Site-uri web", + "source": "Sursă", + "status": "Stare", + "status_completed": "Finalizat", + "status_embedding_completed": "Embedding finalizat", + "status_embedding_failed": "Embedding eșuat", + "status_failed": "Eșuat", + "status_new": "Adăugat", + "status_pending": "În așteptare", + "status_preprocess_completed": "Preprocesare finalizată", + "status_preprocess_failed": "Preprocesare eșuată", + "status_processing": "Se procesează", + "subtitle_file": "fișier subtitrare", + "threshold": "Prag de potrivire", + "threshold_placeholder": "Nesetat", + "threshold_too_large_or_small": "Pragul nu poate fi mai mare de 1 sau mai mic de 0", + "threshold_tooltip": "Folosit pentru a evalua relevanța dintre întrebarea utilizatorului și conținutul din baza de cunoștințe (0-1)", + "title": "Bază de cunoștințe", + "topN": "Număr rezultate returnate", + "topN_placeholder": "Nesetat", + "topN_too_large_or_small": "Numărul de rezultate returnate nu poate fi mai mare de 30 sau mai mic de 1.", + "topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și mai mulți tokeni consumați.", + "url_added": "URL adăugat", + "url_placeholder": "Introdu URL, separă URL-urile multiple prin Enter", + "urls": "URL-uri", + "videos": "video", + "videos_file": "fișier video" + }, + "languages": { + "arabic": "Arabă", + "chinese": "Chineză", + "chinese-traditional": "Chineză tradițională", + "english": "Engleză", + "french": "Franceză", + "german": "Germană", + "indonesian": "Indoneziană", + "italian": "Italiană", + "japanese": "Japoneză", + "korean": "Coreeană", + "malay": "Malaieză", + "polish": "Poloneză", + "portuguese": "Portugheză", + "russian": "Rusă", + "spanish": "Spaniolă", + "thai": "Thailandeză", + "turkish": "Turcă", + "ukrainian": "Ucraineană", + "unknown": "necunoscut", + "urdu": "Urdu", + "vietnamese": "Vietnameză" + }, + "launchpad": { + "apps": "Aplicații", + "minapps": "Mini-aplicații" + }, + "lmstudio": { + "keep_alive_time": { + "description": "Timpul în minute pentru a menține conexiunea activă; implicit este 5 minute.", + "placeholder": "Minute", + "title": "Timp menținere conexiune" + }, + "title": "LM Studio" + }, + "memory": { + "actions": "Acțiuni", + "add_failed": "Nu s-a putut adăuga amintirea", + "add_first_memory": "Adaugă prima ta amintire", + "add_memory": "Adaugă amintire", + "add_new_user": "Adaugă utilizator nou", + "add_success": "Amintire adăugată cu succes", + "add_user": "Adaugă utilizator", + "add_user_failed": "Nu s-a putut adăuga utilizatorul", + "all_users": "Toți utilizatorii", + "cannot_delete_default_user": "Nu se poate șterge utilizatorul implicit", + "configure_memory_first": "Te rugăm să configurezi mai întâi setările de memorie", + "content": "Conținut", + "current_user": "Utilizator curent", + "custom": "Personalizat", + "default": "Implicit", + "default_user": "Utilizator implicit", + "delete_confirm": "Ești sigur că vrei să ștergi această amintire?", + "delete_confirm_content": "Ești sigur că vrei să ștergi {{count}} amintiri?", + "delete_confirm_single": "Ești sigur că vrei să ștergi această amintire?", + "delete_confirm_title": "Șterge amintiri", + "delete_failed": "Nu s-a putut șterge amintirea", + "delete_selected": "Șterge selectate", + "delete_success": "Amintire ștearsă cu succes", + "delete_user": "Șterge utilizator", + "delete_user_confirm_content": "Ești sigur că vrei să ștergi utilizatorul {{user}} și toate amintirile sale?", + "delete_user_confirm_title": "Șterge utilizator", + "delete_user_failed": "Nu s-a putut șterge utilizatorul", + "description": "Memoria îți permite să stochezi și să gestionezi informații despre interacțiunile tale cu asistentul. Poți adăuga, edita și șterge amintiri, precum și să le filtrezi și să cauți prin ele.", + "edit_memory": "Editează amintirea", + "embedding_dimensions": "Dimensiuni embedding", + "embedding_model": "Model embedding", + "enable_global_memory_first": "Te rugăm să activezi mai întâi memoria globală", + "end_date": "Data de sfârșit", + "global_memory": "Memorie globală", + "global_memory_description": "Pentru a folosi funcțiile de memorie, te rugăm să activezi memoria globală în setările asistentului.", + "global_memory_disabled_desc": "Pentru a folosi funcțiile de memorie, te rugăm să activezi mai întâi memoria globală în setările asistentului.", + "global_memory_disabled_title": "Memorie globală dezactivată", + "global_memory_enabled": "Memorie globală activată", + "go_to_memory_page": "Mergi la pagina Memorie", + "initial_memory_content": "Bun venit! Aceasta este prima ta amintire.", + "llm_model": "Model LLM", + "load_failed": "Nu s-au putut încărca amintirile", + "loading": "Se încarcă amintirile...", + "loading_memories": "Se încarcă amintirile...", + "memories_description": "Se afișează {{count}} din {{total}} amintiri", + "memories_reset_success": "Toate amintirile pentru {{user}} au fost resetate cu succes", + "memory": "amintire", + "memory_content": "Conținut amintire", + "memory_placeholder": "Introdu conținutul amintirii...", + "new_user_id": "ID utilizator nou", + "new_user_id_placeholder": "Introdu un ID de utilizator unic", + "no_matching_memories": "Nu s-au găsit amintiri potrivite", + "no_memories": "Încă nu există amintiri", + "no_memories_description": "Începe prin a adăuga prima ta amintire", + "not_configured_desc": "Te rugăm să configurezi modelele de embedding și LLM în setările de memorie pentru a activa funcționalitatea de memorie.", + "not_configured_title": "Memorie neconfigurată", + "pagination_total": "{{start}}-{{end}} din {{total}} elemente", + "please_enter_memory": "Te rugăm să introduci conținutul amintirii", + "please_select_embedding_model": "Te rugăm să selectezi un model de embedding", + "please_select_llm_model": "Te rugăm să selectezi un model LLM", + "reset_filters": "Resetează filtrele", + "reset_memories": "Resetează amintirile", + "reset_memories_confirm_content": "Ești sigur că vrei să ștergi definitiv toate amintirile pentru {{user}}? Această acțiune nu poate fi anulată.", + "reset_memories_confirm_title": "Resetează toate amintirile", + "reset_memories_failed": "Nu s-au putut reseta amintirile", + "reset_user_memories": "Resetează amintirile utilizatorului", + "reset_user_memories_confirm_content": "Ești sigur că vrei să resetezi toate amintirile pentru {{user}}?", + "reset_user_memories_confirm_title": "Resetează amintirile utilizatorului", + "reset_user_memories_failed": "Nu s-au putut reseta amintirile utilizatorului", + "score": "Scor", + "search": "Caută", + "search_placeholder": "Caută amintiri...", + "select_embedding_model_placeholder": "Selectează model embedding", + "select_llm_model_placeholder": "Selectează model LLM", + "select_user": "Selectează utilizator", + "settings": "Setări", + "settings_title": "Setări memorie", + "start_date": "Data de început", + "statistics": "Statistici", + "stored_memories": "Amintiri stocate", + "switch_user": "Schimbă utilizatorul", + "switch_user_confirm": "Schimbi contextul de utilizator la {{user}}?", + "time": "Timp", + "title": "Amintiri", + "total_memories": "total amintiri", + "try_different_filters": "Încearcă să ajustezi criteriile de căutare", + "update_failed": "Nu s-a putut actualiza amintirea", + "update_success": "Amintire actualizată cu succes", + "user": "Utilizator", + "user_created": "Utilizatorul {{user}} a fost creat și comutat cu succes", + "user_deleted": "Utilizatorul {{user}} a fost șters cu succes", + "user_id": "ID utilizator", + "user_id_exists": "Acest ID de utilizator există deja", + "user_id_invalid_chars": "ID-ul de utilizator poate conține doar litere, cifre, cratime și liniuțe de subliniere", + "user_id_placeholder": "Introdu ID utilizator (opțional)", + "user_id_required": "ID-ul de utilizator este obligatoriu", + "user_id_reserved": "'default-user' este rezervat, te rugăm să folosești un ID diferit", + "user_id_rules": "ID-ul de utilizator trebuie să fie unic și să conțină doar litere, cifre, cratime (-) și liniuțe de subliniere (_)", + "user_id_too_long": "ID-ul de utilizator nu poate depăși 50 de caractere", + "user_management": "Gestionare utilizatori", + "user_memories_reset": "Toate amintirile pentru {{user}} au fost resetate", + "user_switch_failed": "Nu s-a putut schimba utilizatorul", + "user_switched": "Contextul de utilizator a fost schimbat la {{user}}", + "users": "utilizatori" + }, + "message": { + "agents": { + "import": { + "error": "Import eșuat" + }, + "imported": "S-au importat cu succes {{count}} asistent/asistenți" + }, + "api": { + "check": { + "model": { + "title": "Selectează modelul de utilizat pentru detectare" + } + }, + "connection": { + "failed": "Conexiune eșuată", + "success": "Conexiune reușită" + } + }, + "assistant": { + "added": { + "content": "Asistent adăugat cu succes" + } + }, + "attachments": { + "pasted_image": "Imagine lipită", + "pasted_text": "Text lipit" + }, + "backup": { + "failed": "Backup eșuat", + "start": { + "success": "Backup început" + }, + "success": "Backup reușit" + }, + "branch": { + "error": "Crearea ramurii a eșuat" + }, + "chat": { + "completion": { + "paused": "Completarea chat-ului a fost pusă în pauză" + } + }, + "citation": "{{count}} citări", + "citations": "Referințe", + "copied": "Copiat!", + "copy": { + "failed": "Copiere eșuată", + "success": "Copiat!" + }, + "delete": { + "confirm": { + "content": "Ești sigur că vrei să ștergi cele {{count}} mesaje selectate?", + "title": "Confirmare ștergere" + }, + "failed": "Ștergere eșuată", + "success": "Ștergere reușită" + }, + "dialog": { + "failed": "Previzualizare eșuată" + }, + "download": { + "failed": "Descărcare eșuată", + "success": "Descărcare reușită" + }, + "empty_url": "Nu s-a putut descărca imaginea, posibil din cauza promptului care conține conținut sensibil sau cuvinte interzise", + "error": { + "chunk_overlap_too_large": "Suprapunerea fragmentelor nu poate fi mai mare decât dimensiunea fragmentului", + "copy": "Copiere eșuată", + "dimension_too_large": "Dimensiunea conținutului este prea mare", + "enter": { + "api": { + "host": "Te rugăm să introduci mai întâi gazda (host) API", + "label": "Te rugăm să introduci mai întâi cheia API" + }, + "model": "Te rugăm să selectezi mai întâi un model", + "name": "Te rugăm să introduci numele bazei de cunoștințe" + }, + "fetchTopicName": "Nu s-a putut numi subiectul", + "get_embedding_dimensions": "Nu s-au putut obține dimensiunile embedding", + "invalid": { + "api": { + "host": "Gazdă (Host) API invalidă", + "label": "Cheie API invalidă" + }, + "enter": { + "model": "Te rugăm să selectezi un model" + }, + "nutstore": "Setări Nutstore invalide", + "nutstore_token": "Token Nutstore invalid", + "proxy": { + "url": "URL proxy invalid" + }, + "webdav": "Setări WebDAV invalide" + }, + "joplin": { + "export": "Nu s-a putut exporta în Joplin. Te rugăm să menții Joplin rulând și să verifici starea conexiunii sau configurarea", + "no_config": "Tokenul de autorizare Joplin sau URL-ul nu sunt configurate" + }, + "markdown": { + "export": { + "preconf": "Nu s-a putut exporta fișierul Markdown în calea preconfigurată", + "specified": "Nu s-a putut exporta fișierul Markdown" + } + }, + "notes": { + "export": "Nu s-au putut exporta notele" + }, + "notion": { + "export": "Nu s-a putut exporta în Notion. Te rugăm să verifici starea conexiunii și configurarea conform documentației", + "no_api_key": "ApiKey-ul Notion sau DatabaseID-ul Notion nu sunt configurate", + "no_content": "Nu există nimic de exportat în Notion." + }, + "siyuan": { + "export": "Nu s-a putut exporta în Siyuan Note, te rugăm să verifici starea conexiunii și configurarea conform documentației", + "no_config": "Adresa API Siyuan Note sau tokenul nu sunt configurate" + }, + "unknown": "Eroare necunoscută", + "yuque": { + "export": "Nu s-a putut exporta în Yuque. Te rugăm să verifici starea conexiunii și configurarea conform documentației", + "no_config": "Tokenul Yuque sau Url-ul Yuque nu sunt configurate" + } + }, + "group": { + "delete": { + "content": "Ștergerea unui mesaj de grup va șterge întrebarea utilizatorului și toate răspunsurile asistentului", + "title": "Șterge mesajul de grup" + }, + "retry_failed": "Reîncearcă mesajele eșuate" + }, + "ignore": { + "knowledge": { + "base": "Modul căutare web este activat, se ignoră baza de cunoștințe" + } + }, + "loading": { + "notion": { + "exporting_progress": "Se exportă în Notion...", + "preparing": "Se pregătește exportul în Notion..." + } + }, + "mention": { + "title": "Schimbă răspunsul modelului" + }, + "message": { + "code_style": "Stil cod", + "compact": { + "title": "Conversație compactată" + }, + "delete": { + "content": "Ești sigur că vrei să ștergi acest mesaj?", + "title": "Șterge mesajul" + }, + "multi_model_style": { + "fold": { + "compress": "Treci la aspect compact", + "expand": "Treci la aspect extins", + "label": "Vizualizare pliată" + }, + "grid": "Aspect grilă", + "horizontal": "Alăturat", + "label": "Stil grup", + "vertical": "Vizualizare stivuită" + }, + "style": { + "bubble": "Bulă", + "label": "Stil mesaj", + "plain": "Simplu" + }, + "video": { + "error": { + "local_file_missing": "Calea fișierului video local nu a fost găsită", + "unsupported_type": "Tip video neacceptat", + "youtube_url_missing": "URL-ul video YouTube nu a fost găsit" + } + } + }, + "processing": "Se procesează...", + "regenerate": { + "confirm": "Regenerarea va înlocui mesajul curent" + }, + "reset": { + "confirm": { + "content": "Ești sigur că vrei să ștergi toate datele?" + }, + "double": { + "confirm": { + "content": "Toate datele vor fi pierdute, vrei să continui?", + "title": "DATE PIERDUTE !!!" + } + } + }, + "restore": { + "failed": "Restaurare eșuată", + "success": "Restaurat cu succes" + }, + "save": { + "success": { + "title": "Salvat cu succes" + } + }, + "searching": "Se caută...", + "success": { + "joplin": { + "export": "Exportat cu succes în Joplin" + }, + "markdown": { + "export": { + "preconf": "Fișierul Markdown a fost exportat cu succes în calea preconfigurată", + "specified": "Fișierul Markdown a fost exportat cu succes" + } + }, + "notes": { + "export": "Exportat cu succes în note" + }, + "notion": { + "export": "Exportat cu succes în Notion" + }, + "siyuan": { + "export": "Exportat cu succes în Siyuan Note" + }, + "yuque": { + "export": "Exportat cu succes în Yuque" + } + }, + "switch": { + "disabled": "Te rugăm să aștepți finalizarea răspunsului curent" + }, + "tools": { + "abort_failed": "Anularea apelului instrumentului a eșuat", + "aborted": "Apelul instrumentului a fost anulat", + "autoApproveEnabled": "Aprobare automată activată pentru acest instrument", + "cancelled": "Anulat", + "completed": "Finalizat", + "error": "A apărut o eroare", + "invoking": "Se invocă", + "pending": "În așteptare", + "preview": "Previzualizare", + "raw": "Brut" + }, + "topic": { + "added": "Subiect nou adăugat" + }, + "upgrade": { + "success": { + "button": "Repornește", + "content": "Te rugăm să repornești aplicația pentru a finaliza actualizarea", + "title": "Actualizare reușită" + } + }, + "warn": { + "export": { + "exporting": "Un alt export este în curs. Te rugăm să aștepți finalizarea exportului anterior și apoi să încerci din nou." + } + }, + "warning": { + "rate": { + "limit": "Prea multe cereri. Te rugăm să aștepți {{seconds}} secunde înainte de a încerca din nou." + } + }, + "websearch": { + "cutoff": "Se trunchiază conținutul căutării...", + "fetch_complete": "{{count}} rezultat(e) căutare", + "rag": "Se execută RAG...", + "rag_complete": "Se păstrează {{countAfter}} din {{countBefore}} rezultate...", + "rag_failed": "RAG a eșuat, se returnează rezultate goale..." + } + }, + "minapp": { + "add_to_launchpad": "Adaugă în Launchpad", + "add_to_sidebar": "Adaugă în bara laterală", + "popup": { + "close": "Închide MinApp", + "devtools": "Instrumente dezvoltator", + "goBack": "Mergi înapoi", + "goForward": "Mergi înainte", + "minimize": "Minimizează MinApp", + "openExternal": "Deschide în browser", + "open_link_external_off": "Curent: Deschide linkurile în fereastra implicită", + "open_link_external_on": "Curent: Deschide linkurile în browser", + "refresh": "Reîmprospătează", + "rightclick_copyurl": "Clic dreapta pentru a copia URL-ul" + }, + "remove_from_launchpad": "Elimină din Launchpad", + "remove_from_sidebar": "Elimină din bara laterală", + "sidebar": { + "close": { + "title": "Închide" + }, + "closeall": { + "title": "Închide tot" + }, + "hide": { + "title": "Ascunde" + }, + "remove_custom": { + "title": "Șterge aplicația personalizată" + } + }, + "title": "MinApp" + }, + "minapps": { + "ant-ling": "Ant Ling", + "baichuan": "Baichuan", + "baidu-ai-search": "Baidu AI Search", + "chatglm": "ChatGLM", + "dangbei": "Dangbei", + "doubao": "Doubao", + "hailuo": "MINIMAX", + "metaso": "Metaso", + "nami-ai": "Nami AI", + "nami-ai-search": "Nami AI Search", + "qwen": "Qwen", + "sensechat": "SenseChat", + "stepfun": "Stepfun", + "tencent-yuanbao": "Yuanbao", + "tiangong-ai": "Skywork", + "wanzhi": "Wanzhi", + "wenxin": "ERNIE", + "wps-copilot": "WPS Copilot", + "xiaoyi": "Xiaoyi", + "zhihu": "Zhihu" + }, + "miniwindow": { + "alert": { + "google_login": "Sfat: Dacă vezi un mesaj 'browser not trusted' când te conectezi la Google, te rugăm să te conectezi mai întâi prin mini-aplicația Google din lista de mini-aplicații, apoi să folosești autentificarea Google în alte mini-aplicații" + }, + "clipboard": { + "empty": "Clipboardul este gol" + }, + "feature": { + "chat": "Răspunde la această întrebare", + "explanation": "Explicație", + "summary": "Rezumat conținut", + "translate": "Traducere text" + }, + "footer": { + "backspace_clear": "Backspace pentru a șterge", + "copy_last_message": "Apasă C pentru a copia", + "esc": "ESC pentru a {{action}}", + "esc_back": "reveni", + "esc_close": "închide", + "esc_pause": "pune pauză" + }, + "input": { + "placeholder": { + "empty": "Cere ajutor de la {{model}}...", + "title": "Ce vrei să faci cu acest text?" + } + }, + "tooltip": { + "pin": "Menține fereastra deasupra" + } + }, + "models": { + "add_parameter": "Adaugă parametru", + "all": "Toate", + "custom_parameters": "Parametri personalizați", + "dimensions": "Dimensiuni {{dimensions}}", + "edit": "Editează modelul", + "embedding": "Embedding", + "embedding_dimensions": "Dimensiuni embedding", + "embedding_model": "Model embedding", + "embedding_model_tooltip": "Adaugă în Setări->Furnizor Model->Gestionează", + "enable_tool_use": "Activează utilizarea instrumentelor", + "filter": { + "by_tag": "Filtrează după etichetă", + "selected": "Etichete selectate" + }, + "function_calling": "Apelare funcții", + "invalid_model": "Model invalid", + "no_matches": "Nu există modele disponibile", + "parameter_name": "Nume parametru", + "parameter_type": { + "boolean": "Boolean", + "json": "JSON", + "number": "Număr", + "string": "Text" + }, + "pinned": "Fixat", + "price": { + "cost": "Cost", + "currency": "Monedă", + "custom": "Personalizat", + "custom_currency": "Monedă personalizată", + "custom_currency_placeholder": "Introdu moneda personalizată", + "input": "Preț intrare", + "million_tokens": "M Tokeni", + "output": "Preț ieșire", + "price": "Preț" + }, + "reasoning": "Raționament", + "rerank_model": "Reranker", + "rerank_model_not_support_provider": "Momentan, modelul reranker nu acceptă acest furnizor ({{provider}})", + "rerank_model_support_provider": "Momentan, modelul reranker acceptă doar anumiți furnizori ({{provider}})", + "rerank_model_tooltip": "Fă clic pe butonul Gestionează din Setări -> Servicii Model pentru a adăuga.", + "search": { + "placeholder": "Caută modele...", + "tooltip": "Caută modele" + }, + "stream_output": "Ieșire flux", + "type": { + "embedding": "Embedding", + "free": "Gratuit", + "function_calling": "Instrument", + "reasoning": "Raționament", + "rerank": "Reranker", + "select": "Tipuri de modele", + "text": "Text", + "vision": "Vizual", + "websearch": "Căutare Web" + } + }, + "navbar": { + "expand": "Extinde dialogul", + "hide_sidebar": "Ascunde bara laterală", + "show_sidebar": "Arată bara laterală", + "window": { + "close": "Închide", + "maximize": "Maximizează", + "minimize": "Minimizează", + "restore": "Restaurează" + } + }, + "navigate": { + "provider_settings": "Mergi la setările furnizorului" + }, + "notes": { + "auto_rename": { + "empty_note": "Notița este goală, nu se poate genera numele", + "failed": "Generarea numelui notiței a eșuat", + "label": "Generează nume notiță", + "success": "Numele notiței a fost generat cu succes" + }, + "characters": "Caractere", + "collapse": "Restrânge", + "content_placeholder": "Te rugăm să introduci conținutul notiței...", + "copyContent": "Copiază conținutul", + "crossPlatformRestoreWarning": "Configurația multi-platformă a fost restaurată, dar directorul de notițe este gol. Te rugăm să copiezi fișierele notițelor în: {{path}}", + "delete": "șterge", + "delete_confirm": "Ești sigur că vrei să ștergi acest {{type}}?", + "delete_folder_confirm": "Ești sigur că vrei să ștergi dosarul \"{{name}}\" și tot conținutul său?", + "delete_note_confirm": "Ești sigur că vrei să ștergi notița \"{{name}}\"?", + "drop_markdown_hint": "Trage fișiere sau dosare .md aici pentru a importa", + "empty": "Încă nu există notițe disponibile", + "expand": "desfășoară", + "export_failed": "Exportul în baza de cunoștințe a eșuat", + "export_knowledge": "Exportă notițele în baza de cunoștințe", + "export_success": "Exportat cu succes în baza de cunoștințe", + "folder": "dosar", + "new_folder": "Dosar nou", + "new_note": "Creează o notiță nouă", + "no_content_to_copy": "Niciun conținut de copiat", + "no_file_selected": "Te rugăm să selectezi fișierul de încărcat", + "no_valid_files": "Nu a fost încărcat niciun fișier valid", + "open_folder": "Deschide un dosar extern", + "open_outside": "Deschide din exterior", + "rename": "Redenumește", + "rename_changed": "Din cauza politicilor de securitate, numele fișierului a fost schimbat din {{original}} în {{final}}", + "save": "Salvează în Notițe", + "search": { + "both": "Nume+Conținut", + "content": "Conținut", + "found_results": "S-au găsit {{count}} rezultate (Nume: {{nameCount}}, Conținut: {{contentCount}})", + "more_matches": "mai multe potriviri", + "searching": "Se caută...", + "show_less": "Arată mai puțin" + }, + "settings": { + "data": { + "apply": "Aplică", + "apply_path_failed": "Nu s-a putut aplica calea", + "current_work_directory": "Director de lucru curent", + "invalid_directory": "Directorul selectat este invalid sau accesul este refuzat", + "path_required": "Te rugăm să selectezi un director de lucru", + "path_updated": "Directorul de lucru a fost actualizat cu succes", + "reset_failed": "Resetarea a eșuat", + "reset_to_default": "Resetează la implicit", + "select": "Selectează", + "select_directory_failed": "Nu s-a putut selecta directorul", + "title": "Setări date", + "work_directory_description": "Directorul de lucru este locul unde sunt stocate toate fișierele notițelor. Schimbarea directorului de lucru nu va muta fișierele existente; te rugăm să migrezi fișierele manual.", + "work_directory_placeholder": "Selectează directorul de lucru pentru notițe" + }, + "display": { + "compress_content": "Compresie conținut", + "compress_content_description": "Când este activat, va limita numărul de caractere pe linie, reducând conținutul afișat pe ecran, dar făcând paragrafele lungi mai ușor de citit.", + "default_font": "Font implicit", + "font_size": "Dimensiune font", + "font_size_description": "Ajustează dimensiunea fontului pentru o experiență de citire mai bună (10-30px)", + "font_size_large": "Mare", + "font_size_medium": "Mediu", + "font_size_small": "Mic", + "font_title": "Setări font", + "serif_font": "Font cu serife", + "show_table_of_contents": "Arată cuprinsul", + "show_table_of_contents_description": "Afișează o bară laterală cu cuprinsul pentru o navigare ușoară în documente", + "title": "Setări afișare" + }, + "editor": { + "edit_mode": { + "description": "În Vizualizarea Editare, modul de editare implicit pentru notițe noi", + "preview_mode": "Previzualizare live", + "source_mode": "Mod cod sursă", + "title": "Vizualizare editare implicită" + }, + "title": "Setări editor", + "view_mode": { + "description": "Mod vizualizare implicit notițe noi", + "edit_mode": "Mod editare", + "read_mode": "Mod citire", + "title": "Vizualizare implicită" + }, + "view_mode_description": "Setează modul de vizualizare implicit pentru pagina filă nouă." + }, + "title": "Notițe" + }, + "show_starred": "Arată notițele favorite", + "sort_a2z": "Nume fișier (A-Z)", + "sort_created_asc": "Ora creării (cele mai vechi întâi)", + "sort_created_desc": "Ora creării (cele mai noi întâi)", + "sort_updated_asc": "Ora actualizării (cele mai vechi întâi)", + "sort_updated_desc": "Ora actualizării (cele mai noi întâi)", + "sort_z2a": "Nume fișier (Z-A)", + "spell_check": "Verificare ortografică", + "spell_check_tooltip": "Activează/Dezactivează verificarea ortografică", + "star": "Notiță favorită", + "starred_notes": "Notițe colectate", + "title": "Notițe", + "unsaved_changes": "Ai conținut nesalvat, ești sigur că vrei să pleci?", + "unstar": "Elimină de la favorite", + "untitled_folder": "Dosar nou", + "untitled_note": "Notiță fără titlu", + "upload_failed": "Încărcarea notiței a eșuat", + "upload_files": "Încarcă fișiere", + "upload_folder": "Încarcă dosar", + "upload_success": "Notiță încărcată cu succes", + "uploading_files": "Se încarcă {{count}} fișiere..." + }, + "notification": { + "assistant": "Răspuns asistent", + "knowledge": { + "error": "{{error}}", + "success": "S-a adăugat cu succes {{type}} în baza de cunoștințe" + }, + "tip": "Dacă răspunsul este de succes, atunci doar mesajele care depășesc 30 de secunde vor declanșa un memento" + }, + "ocr": { + "builtin": { + "system": "OCR de sistem" + }, + "error": { + "provider": { + "cannot_remove_builtin": "Nu se poate șterge furnizorul integrat", + "existing": "Furnizorul există deja", + "get_providers": "Nu s-au putut obține furnizorii disponibili", + "not_found": "Furnizorul OCR nu există", + "update_failed": "Nu s-a putut actualiza configurația" + }, + "unknown": "A apărut o eroare în timpul procesului OCR" + }, + "file": { + "not_supported": "Tip de fișier neacceptat {{type}}" + }, + "processing": "Procesare OCR...", + "warning": { + "provider": { + "fallback": "S-a recurs la {{name}}, ceea ce poate cauza probleme" + } + } + }, + "ollama": { + "keep_alive_time": { + "description": "Timpul în minute pentru a menține conexiunea activă; implicit este 5 minute.", + "placeholder": "Minute", + "title": "Timp menținere conexiune" + }, + "title": "Ollama" + }, + "ovms": { + "action": { + "install": "Instalează", + "installing": "Se instalează", + "reinstall": "Reinstalează", + "run": "Rulează OVMS", + "starting": "Se pornește", + "stop": "Oprește OVMS", + "stopping": "Se oprește" + }, + "description": "

1. Descarcă modele OV.

2. Adaugă modele în 'Manager'.

Suportă doar Windows!

Cale instalare OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Te rugăm să consulți Ghidul Intel OVMS

", + "download": { + "button": "Descarcă", + "error": "Eroare descărcare", + "model_id": { + "label": "ID model:", + "model_id_pattern": "ID-ul modelului trebuie să înceapă cu OpenVINO/", + "placeholder": "Obligatoriu de ex. OpenVINO/Qwen3-8B-int4-ov", + "required": "Te rugăm să introduci ID-ul modelului" + }, + "model_name": { + "label": "Nume model:", + "placeholder": "Obligatoriu de ex. Qwen3-8B-int4-ov", + "required": "Te rugăm să introduci numele modelului" + }, + "model_source": "Sursă model:", + "model_task": "Sarcină model:", + "success": "Descărcare reușită", + "success_desc": "Modelul \"{{modelName}}\"-\"{{modelId}}\" descărcat cu succes, te rugăm să mergi la interfața de gestionare OVMS pentru a adăuga modelul", + "tip": "Modelul se descarcă, uneori durează ore întregi. Te rugăm să ai răbdare...", + "title": "Descarcă model Intel OpenVINO" + }, + "failed": { + "install": "Instalarea OVMS a eșuat:", + "install_code_100": "Eroare necunoscută", + "install_code_101": "Suportă doar procesoare Intel(R)", + "install_code_102": "Suportă doar Windows", + "install_code_103": "Descărcarea runtime-ului OVMS a eșuat", + "install_code_104": "Nu s-a putut instala runtime-ul OVMS", + "install_code_105": "Nu s-a putut crea ovdnd.exe", + "install_code_106": "Nu s-a putut crea run.bat", + "install_code_110": "Nu s-a putut curăța vechiul runtime OVMS", + "run": "Rularea OVMS a eșuat:", + "stop": "Oprirea OVMS a eșuat:" + }, + "status": { + "not_installed": "OVMS nu este instalat", + "not_running": "OVMS nu rulează", + "running": "OVMS rulează", + "unknown": "Stare OVMS necunoscută" + }, + "title": "Intel OVMS" + }, + "paintings": { + "aspect_ratio": "Raport de aspect", + "aspect_ratios": { + "landscape": "Peisaj", + "portrait": "Portret", + "square": "Pătrat" + }, + "auto_create_paint": "Creează automat imagine", + "auto_create_paint_tip": "După ce imaginea este generată, o nouă imagine va fi creată automat.", + "background": "Fundal", + "background_options": { + "auto": "Auto", + "opaque": "Opac", + "transparent": "Transparent" + }, + "button": { + "delete": { + "image": { + "confirm": "Ești sigur că vrei să ștergi această imagine?", + "label": "Șterge imaginea" + } + }, + "new": { + "image": "Imagine nouă" + } + }, + "custom_size": "Dimensiune personalizată", + "edit": { + "image_file": "Imagine editată", + "magic_prompt_option_tip": "Îmbunătățește inteligent prompturile de editare", + "model_tip": "Versiunile V3 și V2 acceptate", + "number_images_tip": "Numărul de rezultate editate de generat", + "rendering_speed_tip": "Controlează compromisul dintre viteza de randare și calitate, disponibil doar pentru V_3", + "seed_tip": "Controlează aleatoriul editării", + "style_type_tip": "Stil pentru imaginea editată, doar pentru V_2 și versiuni ulterioare" + }, + "generate": { + "height": "Înălțime", + "magic_prompt_option_tip": "Îmbunătățește inteligent prompturile pentru rezultate mai bune", + "model_tip": "Versiune model: V3 este cea mai recentă versiune, V2 este modelul anterior, V2A este modelul rapid, V_1 este modelul de prima generație, _TURBO este versiunea accelerată", + "negative_prompt_tip": "Descrie elementele nedorite, doar pentru V_1, V_1_TURBO, V_2 și V_2_TURBO", + "number_images_tip": "Numărul de imagini de generat", + "person_generation": "Generare persoană", + "person_generation_tip": "Permite modelului să genereze imagini cu persoane", + "rendering_speed_tip": "Controlează compromisul dintre viteza de randare și calitate, disponibil doar pentru V_3", + "safety_tolerance": "Toleranță de siguranță", + "safety_tolerance_tip": "Controlează toleranța de siguranță pentru generarea imaginilor, disponibil doar pentru FLUX.1-Kontext-pro", + "seed_tip": "Controlează aleatoriul generării imaginii pentru rezultate reproductibile", + "style_type_tip": "Stil generare imagine pentru V_2 și versiuni ulterioare", + "width": "Lățime" + }, + "generated_image": "Imagine generată", + "go_to_settings": "Mergi la Setări", + "guidance_scale": "Scară de ghidare", + "guidance_scale_tip": "Classifier Free Guidance. Cât de fidel vrei să respecte modelul promptul tău când caută o imagine similară să-ți arate", + "image": { + "size": "Dimensiune imagine" + }, + "image_file_required": "Te rugăm să încarci mai întâi o imagine", + "image_file_retry": "Te rugăm să reîncarci mai întâi o imagine", + "image_handle_required": "Te rugăm să încarci mai întâi o imagine.", + "image_placeholder": "Nicio imagine disponibilă", + "image_retry": "Reîncearcă", + "image_size_options": { + "auto": "Auto" + }, + "inference_steps": "Pași de inferență", + "inference_steps_tip": "Numărul de pași de inferență de efectuat. Mai mulți pași produc o calitate mai mare, dar durează mai mult", + "input_image": "Imagine de intrare", + "input_parameters": "Parametri de intrare", + "learn_more": "Află mai multe", + "magic_prompt_option": "Prompt magic", + "mode": { + "edit": "Editează", + "generate": "Desenează", + "merge": "Îmbină", + "remix": "Remix", + "upscale": "Upscale" + }, + "model": "Model", + "model_and_pricing": "Model și prețuri", + "moderation": "Moderare", + "moderation_options": { + "auto": "Auto", + "low": "Scăzut" + }, + "negative_prompt": "Prompt negativ", + "negative_prompt_tip": "Descrie ce nu vrei să fie inclus în imagine", + "no_image_generation_model": "Niciun model de generare imagini disponibil, te rugăm să adaugi un model și să setezi tipul endpoint-ului la {{endpoint_type}}", + "number_images": "Număr imagini", + "number_images_tip": "Numărul de imagini de generat (1-4)", + "paint_course": "tutorial", + "per_image": "pe imagine", + "per_images": "pe imagini", + "person_generation_options": { + "allow_adult": "Permite adulți", + "allow_all": "Permite tot", + "allow_none": "Nepermis" + }, + "pricing": "Prețuri", + "prompt_enhancement": "Îmbunătățire prompt", + "prompt_enhancement_tip": "Rescrie prompturile în versiuni detaliate, optimizate pentru model, când este activat", + "prompt_placeholder": "Descrie imaginea pe care vrei să o creezi, de ex. Un lac senin la apus cu munți în fundal", + "prompt_placeholder_edit": "Introdu descrierea imaginii, desenarea textului folosește \"ghilimele duble\" pentru încadrare", + "prompt_placeholder_en": "Introdu descrierea imaginii, momentan acceptă doar prompturi în engleză", + "proxy_required": "Deschide proxy-ul și activează \"Modul TUN\" pentru a vizualiza imaginile generate sau copiază-le în browser pentru deschidere. În viitor, conexiunea directă internă va fi acceptată", + "quality": "Calitate", + "quality_options": { + "auto": "Auto", + "high": "Înaltă", + "low": "Scăzută", + "medium": "Medie" + }, + "regenerate": { + "confirm": "Aceasta va înlocui imaginile generate existente. Vrei să continui?" + }, + "remix": { + "image_file": "Imagine de referință", + "image_weight": "Pondere imagine de referință", + "image_weight_tip": "Ajustează influența imaginii de referință", + "magic_prompt_option_tip": "Îmbunătățește inteligent prompturile de remix", + "model_tip": "Selectează versiunea modelului AI pentru remixare", + "negative_prompt_tip": "Descrie elementele nedorite în rezultatele remix", + "number_images_tip": "Numărul de rezultate remix de generat", + "rendering_speed_tip": "Controlează compromisul dintre viteza de randare și calitate, disponibil doar pentru V_3", + "seed_tip": "Controlează aleatoriul rezultatului combinat", + "style_type_tip": "Stil pentru imaginea remixată, doar pentru V_2 și versiuni ulterioare" + }, + "rendering_speed": "Viteză de randare", + "rendering_speeds": { + "default": "Implicit", + "quality": "Calitate", + "turbo": "Turbo" + }, + "req_error_model": "Nu s-a putut prelua modelul", + "req_error_no_balance": "Te rugăm să verifici validitatea tokenului", + "req_error_text": "Serverul este ocupat sau promptul conține termeni \"protejați prin drepturi de autor\" sau \"sensibili\". Te rugăm să încerci din nou.", + "req_error_token": "Te rugăm să verifici validitatea tokenului", + "required_field": "Câmp obligatoriu", + "seed": "Seed", + "seed_desc_tip": "Același seed și prompt pot genera imagini similare; setarea -1 va genera rezultate diferite de fiecare dată", + "seed_tip": "Același seed și prompt pot produce imagini similare", + "select_model": "Selectează modelul", + "style_type": "Stil", + "style_types": { + "3d": "3D", + "anime": "Anime", + "auto": "Auto", + "design": "Design", + "general": "General", + "realistic": "Realist" + }, + "text_desc_required": "Te rugăm să introduci mai întâi descrierea imaginii", + "title": "Imagini", + "top_up": "Reîncarcă ", + "translating": "Se traduce...", + "uploaded_input": "Intrare încărcată", + "upscale": { + "detail": "Detaliu", + "detail_tip": "Controlează nivelul de îmbunătățire a detaliilor", + "image_file": "Imagine de scalat", + "magic_prompt_option_tip": "Îmbunătățește inteligent prompturile de upscaling", + "number_images_tip": "Numărul de rezultate scalate de generat", + "resemblance": "Similaritate", + "resemblance_tip": "Controlează similaritatea cu imaginea originală", + "seed_tip": "Controlează aleatoriul scalării" + } + }, + "plugins": { + "actions": "Acțiuni", + "agents": "Agenți", + "all_categories": "Toate categoriile", + "all_types": "Toate", + "category": "Categorie", + "commands": "Comenzi", + "confirm_uninstall": "Ești sigur că vrei să dezinstalezi {{name}}?", + "install": "Instalează", + "install_plugins_from_browser": "Răsfoiește pluginurile disponibile pentru a începe", + "installing": "Se instalează...", + "name": "Nume", + "no_description": "Nicio descriere disponibilă", + "no_installed_plugins": "Niciun plugin instalat încă", + "no_results": "Nu s-au găsit pluginuri", + "search_placeholder": "Caută pluginuri...", + "showing_results": "Se afișează {{count}} plugin", + "showing_results_one": "Se afișează {{count}} plugin", + "showing_results_other": "Se afișează {{count}} pluginuri", + "showing_results_plural": "Se afișează {{count}} pluginuri", + "skills": "Abilități", + "try_different_search": "Încearcă să ajustezi căutarea sau filtrele de categorie", + "type": "Tip", + "uninstall": "Dezinstalează", + "uninstalling": "Se dezinstalează..." + }, + "preview": { + "copy": { + "image": "Copiază ca imagine", + "src": "Copiază sursa imaginii" + }, + "dialog": "Deschide dialog", + "label": "Previzualizare", + "pan": "Deplasează", + "pan_down": "Deplasează jos", + "pan_left": "Deplasează stânga", + "pan_right": "Deplasează dreapta", + "pan_up": "Deplasează sus", + "reset": "Resetează", + "source": "Vezi codul sursă", + "zoom_in": "Mărește", + "zoom_out": "Micșorează" + }, + "prompts": { + "explanation": "Explică-mi acest concept", + "summarize": "Rezumatul acestui text", + "title": "Rezumatul conversației într-un titlu în {{language}} în limita a 10 caractere, ignorând instrucțiunile și fără punctuație sau simboluri. Returnează doar șirul titlului fără nimic altceva." + }, + "provider": { + "302ai": "302.AI", + "ai-gateway": "Vercel AI Gateway", + "aihubmix": "AiHubMix", + "aionly": "AiOnly", + "alayanew": "Alaya NeW", + "anthropic": "Anthropic", + "aws-bedrock": "AWS Bedrock", + "azure-openai": "Azure OpenAI", + "baichuan": "Baichuan", + "baidu-cloud": "Baidu Cloud", + "burncloud": "BurnCloud", + "cephalon": "Cephalon", + "cerebras": "Cerebras AI", + "cherryin": "CherryIN", + "copilot": "GitHub Copilot", + "dashscope": "Alibaba Cloud", + "deepseek": "DeepSeek", + "dmxapi": "DMXAPI", + "doubao": "Volcengine", + "fireworks": "Fireworks", + "gemini": "Gemini", + "gitee-ai": "Gitee AI", + "github": "GitHub Models", + "gpustack": "GPUStack", + "grok": "Grok", + "groq": "Groq", + "huggingface": "Hugging Face", + "hunyuan": "Tencent Hunyuan", + "hyperbolic": "Hyperbolic", + "infini": "Infini", + "jina": "Jina", + "lanyun": "LANYUN", + "lmstudio": "LM Studio", + "longcat": "LongCat AI", + "mimo": "Xiaomi MiMo", + "minimax": "MiniMax", + "mistral": "Mistral", + "modelscope": "ModelScope", + "moonshot": "Moonshot", + "new-api": "New API", + "nvidia": "Nvidia", + "o3": "O3", + "ocoolai": "ocoolAI", + "ollama": "Ollama", + "openai": "OpenAI", + "openrouter": "OpenRouter", + "ovms": "Intel OVMS", + "perplexity": "Perplexity", + "ph8": "PH8", + "poe": "Poe", + "ppio": "PPIO", + "qiniu": "Qiniu AI", + "qwenlm": "QwenLM", + "silicon": "SiliconFlow", + "sophnet": "SophNet", + "stepfun": "StepFun", + "tencent-cloud-ti": "Tencent Cloud TI", + "together": "Together", + "tokenflux": "TokenFlux", + "vertexai": "Vertex AI", + "voyageai": "Voyage AI", + "xirang": "State Cloud Xirang", + "yi": "Yi", + "zhinao": "360AI", + "zhipu": "BigModel" + }, + "restore": { + "confirm": { + "button": "Selectează fișierul de backup", + "label": "Ești sigur că vrei să restaurezi datele?" + }, + "content": "Operațiunea de restaurare va suprascrie toate datele actuale ale aplicației cu datele din backup. Te rugăm să reții că procesul de restaurare poate dura ceva timp, îți mulțumim pentru răbdare.", + "progress": { + "completed": "Restaurare finalizată", + "copying_files": "Se copiază fișierele... {{progress}}%", + "extracted": "Extragere reușită", + "extracting": "Se extrage backup-ul...", + "preparing": "Se pregătește restaurarea...", + "reading_data": "Se citesc datele...", + "title": "Progres restaurare" + }, + "title": "Restaurare date" + }, + "richEditor": { + "action": { + "table": { + "deleteColumn": "Șterge coloane", + "deleteRow": "Șterge rânduri", + "insertColumnAfter": "Inserează după", + "insertColumnBefore": "Inserează înainte", + "insertRowAfter": "Inserează dedesubt", + "insertRowBefore": "Inserează deasupra" + } + }, + "commands": { + "blockMath": { + "description": "Inserează formulă matematică", + "title": "Bloc matematic" + }, + "blockquote": { + "description": "Capturează un citat", + "title": "Citat" + }, + "bold": { + "description": "Marcat cu aldine", + "title": "Aldine" + }, + "bulletList": { + "description": "Creează o listă simplă cu marcatori", + "title": "Listă cu marcatori" + }, + "calloutInfo": { + "description": "Adaugă o casetă de informații", + "title": "Casetă informații" + }, + "calloutWarning": { + "description": "Adaugă o casetă de avertizare", + "title": "Casetă avertizare" + }, + "code": { + "description": "Inserează fragment de cod", + "title": "Cod" + }, + "codeBlock": { + "description": "Capturează un fragment de cod", + "title": "Cod" + }, + "columns": { + "description": "Creează aspect pe coloane", + "title": "Coloane" + }, + "date": { + "description": "Inserează data curentă", + "title": "Dată" + }, + "divider": { + "description": "Adaugă o linie orizontală", + "title": "Divizor" + }, + "hardBreak": { + "description": "Inserează o întrerupere de linie", + "title": "Întrerupere de linie" + }, + "heading1": { + "description": "Titlu secțiune mare", + "title": "Titlu 1" + }, + "heading2": { + "description": "Titlu secțiune mediu", + "title": "Titlu 2" + }, + "heading3": { + "description": "Titlu secțiune mic", + "title": "Titlu 3" + }, + "heading4": { + "description": "Titlu secțiune mai mic", + "title": "Titlu 4" + }, + "heading5": { + "description": "Titlu secțiune și mai mic", + "title": "Titlu 5" + }, + "heading6": { + "description": "Cel mai mic titlu de secțiune", + "title": "Titlu 6" + }, + "image": { + "description": "Inserează o imagine", + "title": "Imagine" + }, + "inlineCode": { + "description": "Adaugă cod în linie", + "title": "Cod în linie" + }, + "inlineMath": { + "description": "Inserează formule matematice în linie", + "title": "Matematică în linie" + }, + "italic": { + "description": "Marcat ca italic", + "title": "Italic" + }, + "link": { + "description": "Adaugă un link", + "title": "Link" + }, + "noCommandsFound": "Nicio comandă găsită", + "orderedList": { + "description": "Creează o listă numerotată", + "title": "Listă numerotată" + }, + "paragraph": { + "description": "Începe să scrii cu text simplu", + "title": "Text" + }, + "redo": { + "description": "Refă ultima acțiune", + "title": "Refă" + }, + "strike": { + "description": "Marchează ca tăiat", + "title": "Tăiat" + }, + "table": { + "description": "Inserează un tabel", + "title": "Tabel" + }, + "taskList": { + "description": "Creează o listă de sarcini", + "title": "Listă de sarcini" + }, + "underline": { + "description": "Marchează ca subliniat", + "title": "Subliniat" + }, + "undo": { + "description": "Anulează ultima acțiune", + "title": "Anulează" + } + }, + "dragHandle": "Trage pentru a muta", + "frontMatter": { + "addProperty": "Adaugă o proprietate", + "addTag": "Adaugă etichetă", + "changeToBoolean": "Casetă de bifare", + "changeToDate": "Dată", + "changeToNumber": "Număr", + "changeToTags": "Etichete", + "changeToText": "Text", + "changeType": "Schimbă tipul", + "deleteProperty": "Șterge proprietatea", + "editValue": "Editează valoarea", + "empty": "Gol", + "moreActions": "Mai multe acțiuni", + "propertyName": "Nume proprietate" + }, + "image": { + "placeholder": "Adaugă o poză" + }, + "imageUploader": { + "embedImage": "Încorporează imagine", + "embedLink": "Încorporează link", + "embedSuccess": "Imagine încorporată cu succes", + "invalidType": "Te rugăm să selectezi un fișier imagine", + "invalidUrl": "URL imagine invalid", + "processing": "Se procesează imaginea...", + "title": "Adaugă o imagine", + "tooLarge": "Dimensiunea imaginii nu poate depăși 10MB", + "upload": "Încarcă", + "uploadError": "Încărcarea imaginii a eșuat", + "uploadFile": "Încarcă fișier", + "uploadHint": "Suportă JPG, PNG, GIF și alte formate, max 10MB", + "uploadSuccess": "Imagine încărcată cu succes", + "uploadText": "Fă clic sau trage imaginea aici pentru a încărca", + "uploading": "Se încarcă imaginea", + "urlPlaceholder": "Lipește linkul imaginii", + "urlRequired": "Te rugăm să introduci URL-ul imaginii" + }, + "link": { + "remove": "Elimină linkul", + "text": "Titlu link", + "textPlaceholder": "Te rugăm să introduci titlul linkului", + "url": "URL link" + }, + "math": { + "placeholder": "Introdu formula LaTeX" + }, + "placeholder": "Scrie '/' pentru comenzi", + "plusButton": "Fă clic pentru a adăuga dedesubt", + "toolbar": { + "blockMath": "Bloc matematic", + "blockquote": "Citat", + "bold": "Aldine", + "bulletList": "Listă cu marcatori", + "clearMarks": "Șterge formatarea", + "code": "Cod în linie", + "codeBlock": "Bloc de cod", + "heading1": "Titlu 1", + "heading2": "Titlu 2", + "heading3": "Titlu 3", + "heading4": "Titlu 4", + "heading5": "Titlu 5", + "heading6": "Titlu 6", + "image": "Imagine", + "inlineMath": "Ecuație în linie", + "italic": "Italic", + "link": "Link", + "orderedList": "Listă ordonată", + "paragraph": "Paragraf", + "redo": "Refă", + "strike": "Tăiat", + "table": "Tabel", + "taskList": "Listă de sarcini", + "underline": "Subliniat", + "undo": "Anulează" + } + }, + "selection": { + "action": { + "builtin": { + "copy": "Copiază", + "explain": "Explică", + "quote": "Citează", + "refine": "Rafinează", + "search": "Caută", + "summary": "Rezumat", + "translate": "Tradu" + }, + "translate": { + "smart_translate_tips": "Traducere inteligentă: Conținutul va fi tradus mai întâi în limba țintă; conținutul aflat deja în limba țintă va fi tradus în limba alternativă" + }, + "window": { + "c_copy": "C: Copiază", + "esc_close": "Esc: Închide", + "esc_stop": "Esc: Oprește", + "opacity": "Opacitate fereastră", + "original_copy": "Copiază originalul", + "original_hide": "Ascunde originalul", + "original_show": "Arată originalul", + "pin": "Fixează", + "pinned": "Fixat", + "r_regenerate": "R: Regenerează" + } + }, + "name": "Asistent de selecție", + "settings": { + "actions": { + "add_tooltip": { + "disabled": "Numărul maxim de acțiuni personalizate a fost atins ({{max}})", + "enabled": "Adaugă acțiune personalizată" + }, + "custom": "Acțiune personalizată", + "delete_confirm": "Ești sigur că vrei să ștergi această acțiune personalizată?", + "drag_hint": "Trage pentru a reordona. Mută deasupra pentru a activa acțiunea ({{enabled}}/{{max}})", + "reset": { + "button": "Resetează", + "confirm": "Ești sigur că vrei să resetezi la acțiunile implicite? Acțiunile personalizate nu vor fi șterse.", + "tooltip": "Resetează la acțiunile implicite. Acțiunile personalizate nu vor fi șterse." + }, + "title": "Acțiuni" + }, + "advanced": { + "filter_list": { + "description": "Funcție avansată, recomandată utilizatorilor cu experiență", + "title": "Listă de filtrare" + }, + "filter_mode": { + "blacklist": "Listă neagră", + "default": "Oprit", + "description": "Poate limita asistentul de selecție să funcționeze doar în anumite aplicații (listă albă) sau să nu funcționeze (listă neagră)", + "title": "Filtru aplicații", + "whitelist": "Listă albă" + }, + "title": "Avansat" + }, + "enable": { + "description": "Momentan acceptat doar pe Windows și macOS", + "mac_process_trust_hint": { + "button": { + "go_to_settings": "Mergi la Setări", + "open_accessibility_settings": "Deschide setările de accesibilitate" + }, + "description": { + "0": "Asistentul de selecție necesită Permisiune de accesibilitate pentru a funcționa corect.", + "1": "Te rugăm să faci clic pe \"Mergi la Setări\" și să apeși butonul \"Deschide setările de sistem\" în fereastra pop-up de solicitare a permisiunii care apare ulterior. Apoi găsește \"Cherry Studio\" în lista de aplicații și activează comutatorul de permisiune.", + "2": "După finalizarea setărilor, te rugăm să redeschizi asistentul de selecție." + }, + "title": "Permisiune de accesibilitate" + }, + "title": "Activează" + }, + "experimental": "Funcții experimentale", + "filter_modal": { + "title": "Listă filtrare aplicații", + "user_tips": { + "mac": "Te rugăm să introduci Bundle ID-ul aplicației, unul pe linie, nu este sensibil la majuscule/minuscule, poate fi potrivit aproximativ. De exemplu: com.google.Chrome, com.apple.mail etc.", + "windows": "Te rugăm să introduci numele fișierului executabil al aplicației, unul pe linie, nu este sensibil la majuscule/minuscule, poate fi potrivit aproximativ. De exemplu: chrome.exe, weixin.exe, Cherry Studio.exe etc." + } + }, + "search_modal": { + "custom": { + "name": { + "hint": "Te rugăm să introduci numele motorului de căutare", + "label": "Nume personalizat", + "max_length": "Numele nu poate depăși 16 caractere" + }, + "test": "Test", + "url": { + "hint": "Folosește {{queryString}} pentru a reprezenta termenul de căutare", + "invalid_format": "Te rugăm să introduci un URL valid care începe cu http:// sau https://", + "label": "URL căutare personalizată", + "missing_placeholder": "URL-ul trebuie să conțină substituentul {{queryString}}", + "required": "Te rugăm să introduci URL-ul de căutare" + } + }, + "engine": { + "custom": "Personalizat", + "label": "Motor de căutare" + }, + "title": "Setează motorul de căutare" + }, + "toolbar": { + "compact_mode": { + "description": "În modul compact, sunt afișate doar pictogramele, fără text", + "title": "Mod compact" + }, + "title": "Bară de instrumente", + "trigger_mode": { + "ctrlkey": "Tasta Ctrl", + "ctrlkey_note": "După selecție, ține apăsată tasta Ctrl pentru a afișa bara de instrumente", + "description": "Modul de declanșare a asistentului de selecție și de afișare a barei de instrumente", + "description_note": { + "mac": "Dacă ai remapat tasta ⌘ folosind scurtături sau instrumente de mapare a tastaturii, acest lucru poate cauza eșecul selecției textului în unele aplicații.", + "windows": "Unele aplicații nu acceptă selectarea textului cu tasta Ctrl. Dacă ai remapat tasta Ctrl folosind instrumente precum AHK, acest lucru poate cauza eșecul selecției textului în unele aplicații." + }, + "selected": "Selecție", + "selected_note": "Arată bara de instrumente imediat ce textul este selectat", + "shortcut": "Comandă rapidă", + "shortcut_link": "Mergi la Setările comenzilor rapide", + "shortcut_note": "După selecție, folosește comanda rapidă pentru a afișa bara de instrumente. Te rugăm să setezi comanda rapidă în pagina de setări și să o activezi. ", + "title": "Mod de declanșare" + } + }, + "user_modal": { + "assistant": { + "default": "Implicit", + "label": "Selectează asistentul" + }, + "icon": { + "error": "Nume pictogramă invalid, te rugăm să verifici intrarea", + "label": "Pictogramă", + "placeholder": "Introdu numele pictogramei Lucide", + "random": "Pictogramă aleatorie", + "tooltip": "Numele pictogramelor Lucide sunt cu litere mici, de ex. arrow-right", + "view_all": "Vezi toate pictogramele" + }, + "model": { + "assistant": "Folosește asistent", + "default": "Model implicit", + "label": "Model", + "tooltip": "Folosind Asistent: Va folosi atât promptul de sistem al asistentului, cât și parametrii modelului" + }, + "name": { + "hint": "Te rugăm să introduci numele acțiunii", + "label": "Nume" + }, + "prompt": { + "copy_placeholder": "Copiază substituentul", + "label": "Prompt utilizator", + "placeholder": "Folosește substituentul {{text}} pentru a reprezenta textul selectat. Dacă este gol, textul selectat va fi adăugat la acest prompt", + "placeholder_text": "Substituent", + "tooltip": "Promptul utilizatorului servește ca o completare la intrarea utilizatorului și nu va suprascrie promptul de sistem al asistentului" + }, + "title": { + "add": "Adaugă acțiune personalizată", + "edit": "Editează acțiunea personalizată" + } + }, + "window": { + "auto_close": { + "description": "Închide automat fereastra când nu este fixată și pierde focusul", + "title": "Închidere automată" + }, + "auto_pin": { + "description": "Fixează fereastra în mod implicit", + "title": "Fixare automată" + }, + "follow_toolbar": { + "description": "Poziția ferestrei va urmări bara de instrumente. Când este dezactivat, va fi întotdeauna centrată.", + "title": "Urmărește bara de instrumente" + }, + "opacity": { + "description": "Setează opacitatea implicită a ferestrei, 100% este complet opac", + "title": "Opacitate" + }, + "remember_size": { + "description": "Fereastra se va afișa la ultima dimensiune ajustată în timpul rulării aplicației", + "title": "Memorează dimensiunea" + }, + "title": "Fereastră de acțiune" + } + } + }, + "settings": { + "about": { + "checkUpdate": { + "available": "Actualizare", + "label": "Verifică actualizări" + }, + "checkingUpdate": "Se verifică actualizările...", + "contact": { + "button": "E-mail", + "title": "Contact" + }, + "debug": { + "open": "Deschide", + "title": "Depanare" + }, + "description": "Un asistent AI puternic pentru producători", + "downloading": "Se descarcă...", + "enterprise": { + "title": "Enterprise" + }, + "feedback": { + "button": "Feedback", + "title": "Feedback" + }, + "label": "Despre și feedback", + "releases": { + "button": "Lansări", + "title": "Note de lansare" + }, + "social": { + "title": "Conturi sociale" + }, + "title": "Despre", + "updateAvailable": "S-a găsit o nouă versiune {{version}}", + "updateError": "Eroare actualizare", + "updateNotAvailable": "Utilizezi cea mai recentă versiune", + "website": { + "button": "Site web", + "title": "Site oficial" + } + }, + "advanced": { + "auto_switch_to_topics": "Comutare automată la subiect", + "title": "Setări avansate" + }, + "assistant": { + "icon": { + "type": { + "emoji": "Pictogramă Emoji", + "label": "Tip pictogramă model", + "model": "Pictogramă model", + "none": "Ascunde" + } + }, + "label": "Asistent implicit", + "model_params": "Parametri model", + "title": "Asistent implicit" + }, + "data": { + "app_data": { + "copy_data_option": "Copiază datele, va reporni automat după copierea datelor din directorul original în noul director", + "copy_failed": "Copierea datelor a eșuat", + "copy_success": "Datele au fost copiate cu succes în noua locație", + "copy_time_notice": "Copierea datelor poate dura ceva timp, nu închide forțat aplicația", + "copying": "Se copiază datele în noua locație...", + "copying_warning": "Se copiază datele, nu închide forțat aplicația; aplicația va reporni după copiere", + "label": "Date aplicație", + "migration_title": "Migrare date", + "new_path": "Cale nouă", + "original_path": "Cale originală", + "path_change_failed": "Schimbarea directorului de date a eșuat", + "path_changed_without_copy": "Calea a fost schimbată cu succes", + "restart_notice": "Aplicația poate necesita repornirea de mai multe ori pentru a aplica modificările", + "select": "Modifică directorul", + "select_error": "Schimbarea directorului de date a eșuat", + "select_error_in_app_path": "Noua cale este aceeași cu calea de instalare a aplicației, te rugăm să selectezi o altă cale", + "select_error_root_path": "Noua cale nu poate fi calea rădăcină", + "select_error_same_path": "Noua cale este aceeași cu vechea cale, te rugăm să selectezi o altă cale", + "select_error_write_permission": "Noua cale nu are permisiuni de scriere", + "select_not_empty_dir": "Noua cale nu este goală", + "select_not_empty_dir_content": "Noua cale nu este goală, va suprascrie datele din noua cale și există riscul de pierdere a datelor și de eșec al copierii. Continui?", + "select_success": "Directorul de date a fost schimbat, aplicația va reporni pentru a aplica modificările", + "select_title": "Schimbă directorul de date al aplicației", + "stop_quit_app_reason": "Aplicația migrează datele momentan și nu poate fi închisă" + }, + "app_knowledge": { + "button": { + "delete": "Șterge fișierul" + }, + "label": "Fișiere bază de cunoștințe", + "remove_all": "Elimină fișierele bazei de cunoștințe", + "remove_all_confirm": "Ștergerea fișierelor bazei de cunoștințe va reduce spațiul de stocare ocupat, dar nu va șterge datele vectoriale ale bazei de cunoștințe; după ștergere, fișierul sursă nu va mai putea fi deschis. Continui?", + "remove_all_success": "Fișiere eliminate cu succes" + }, + "app_logs": { + "button": "Deschide jurnalele", + "label": "Jurnale aplicație" + }, + "backup": { + "skip_file_data_help": "Omite salvarea fișierelor de date precum imagini și baze de cunoștințe în timpul backup-ului și salvează doar înregistrările de chat și setările. Reduce ocuparea spațiului și accelerează viteza de backup.", + "skip_file_data_title": "Backup simplificat" + }, + "clear_cache": { + "button": "Golește memoria cache", + "confirm": "Golirea memoriei cache va șterge datele cache ale aplicației, inclusiv datele minapp. Această acțiune este ireversibilă, continui?", + "error": "Eroare la golirea memoriei cache", + "success": "Memoria cache a fost golită", + "title": "Golește memoria cache" + }, + "data": { + "title": "Director de date" + }, + "divider": { + "basic": "Setări date de bază", + "cloud_storage": "Setări backup în cloud", + "export_settings": "Setări export", + "import_settings": "Setări import", + "third_party": "Conexiuni terțe" + }, + "export_menu": { + "docx": "Exportă ca Word", + "image": "Exportă ca imagine", + "joplin": "Exportă în Joplin", + "markdown": "Exportă ca Markdown", + "markdown_reason": "Exportă ca Markdown (cu raționament)", + "notes": "Exportă în Notițe", + "notion": "Exportă în Notion", + "obsidian": "Exportă în Obsidian", + "plain_text": "Copiază ca text simplu", + "siyuan": "Exportă în SiYuan Note", + "title": "Setări meniu export", + "yuque": "Exportă în Yuque" + }, + "export_to_phone": { + "confirm": { + "button": "Selectează fișierul de backup" + }, + "content": "Exportă unele date, inclusiv jurnalele de chat și setările. Te rugăm să reții că procesul de backup poate dura ceva timp. Îți mulțumim pentru răbdare.", + "lan": { + "connected": "Conectat", + "connection_failed": "Conexiune eșuată", + "content": "Te rugăm să te asiguri că computerul și telefonul sunt în aceeași rețea pentru transferul LAN.", + "device_list_title": "Dispozitive în rețeaua locală", + "discovered_devices": "Dispozitive descoperite", + "error": { + "file_too_large": "Fișier prea mare, maxim 500MB acceptat", + "init_failed": "Inițializare eșuată", + "invalid_file_type": "Doar fișierele ZIP sunt acceptate", + "no_file": "Niciun fișier selectat", + "no_ip": "Nu se poate obține adresa IP", + "not_connected": "Te rugăm să finalizezi handshake-ul mai întâi", + "send_failed": "Trimiterea fișierului a eșuat" + }, + "file_transfer": { + "cancelled": "Transfer anulat", + "failed": "Transfer fișier eșuat: {{message}}", + "progress": "Se trimite... {{progress}}%", + "success": "Fișier trimis cu succes" + }, + "handshake": { + "button": "Handshake", + "failed": "Handshake eșuat: {{message}}", + "in_progress": "Se efectuează handshake...", + "success": "Handshake finalizat cu {{device}}", + "test_message_received": "Primit pong de la {{device}}", + "test_message_sent": "Trimis payload test hello world" + }, + "idle_hint": "Scanare în pauză. Începe scanarea pentru a găsi parteneri Cherry Studio în LAN.", + "ip_addresses": "Adrese IP", + "last_seen": "Văzut ultima dată la {{time}}", + "metadata": "Metadate", + "no_connection_warning": "Te rugăm să deschizi Transfer LAN pe mobil în Cherry Studio", + "no_devices": "Încă nu s-au găsit parteneri LAN", + "scan_devices": "Scanează dispozitive", + "scanning_hint": "Se scanează rețeaua locală pentru parteneri Cherry Studio...", + "send_file": "Trimite fișier", + "status": { + "completed": "Transfer finalizat", + "connected": "Conectat", + "connecting": "Se conectează...", + "disconnected": "Deconectat", + "error": "Eroare de conexiune", + "initializing": "Se inițializează conexiunea...", + "preparing": "Se pregătește transferul...", + "sending": "Se transferă {{progress}}%" + }, + "status_badge_idle": "Inactiv", + "status_badge_scanning": "Se scanează", + "stop_scan": "Oprește scanarea", + "title": "Transmisie LAN", + "transfer_progress": "Progres transfer" + }, + "title": "Exportă pe telefon" + }, + "hour_interval_one": "{{count}} oră", + "hour_interval_other": "{{count}} ore", + "import_settings": { + "button": "Importă fișier Json", + "chatgpt": "Importă din ChatGPT", + "title": "Importă date din aplicație externă" + }, + "joplin": { + "check": { + "button": "Verifică", + "empty_token": "Te rugăm să introduci tokenul de autorizare Joplin", + "empty_url": "Te rugăm să introduci URL-ul serviciului Joplin Clipper", + "fail": "Verificarea conexiunii Joplin a eșuat", + "success": "Verificarea conexiunii Joplin a reușit" + }, + "export_reasoning": { + "help": "Când este activat, conținutul exportat va include lanțul de raționament (procesul de gândire).", + "title": "Include lanțul de raționament în export" + }, + "help": "În opțiunile Joplin, activează web clipper-ul (nu este necesară extensia de browser), confirmă portul și copiază tokenul de autentificare aici.", + "title": "Configurare Joplin", + "token": "Token de autorizare Joplin", + "token_placeholder": "Token de autorizare Joplin", + "url": "URL serviciu Joplin Web Clipper", + "url_placeholder": "http://127.0.0.1:41184/" + }, + "limit": { + "appDataDiskQuota": "Avertisment spațiu pe disc", + "appDataDiskQuotaDescription": "Spațiul directorului de date este aproape plin, te rugăm să eliberezi spațiu pe disc, altfel datele se vor pierde" + }, + "local": { + "autoSync": { + "label": "Backup automat", + "off": "Oprit" + }, + "backup": { + "button": "Backup local", + "manager": { + "columns": { + "actions": "Acțiuni", + "fileName": "Nume fișier", + "modifiedTime": "Ora modificării", + "size": "Dimensiune" + }, + "delete": { + "confirm": { + "multiple": "Ești sigur că vrei să ștergi cele {{count}} fișiere de backup selectate? Această acțiune nu poate fi anulată.", + "single": "Ești sigur că vrei să ștergi fișierul de backup \"{{fileName}}\"? Această acțiune nu poate fi anulată.", + "title": "Confirmă ștergerea" + }, + "error": "Ștergerea a eșuat", + "selected": "Șterge selectate", + "success": { + "multiple": "S-au șters cu succes {{count}} fișiere de backup", + "single": "Șters cu succes" + }, + "text": "Șterge" + }, + "fetch": { + "error": "Nu s-au putut obține fișierele de backup" + }, + "refresh": "Reîmprospătează", + "restore": { + "error": "Restaurare eșuată", + "success": "Restaurare reușită, aplicația se va reîmprospăta în scurt timp", + "text": "Restaurează" + }, + "select": { + "files": { + "delete": "Te rugăm să selectezi fișierele de backup de șters" + } + }, + "title": "Manager backup local" + }, + "modal": { + "filename": { + "placeholder": "Te rugăm să introduci numele fișierului de backup" + }, + "title": "Backup în director local" + } + }, + "directory": { + "label": "Director backup local", + "placeholder": "Selectează un director pentru backup-uri locale", + "select_error_app_data_path": "Noua cale nu poate fi aceeași cu calea datelor aplicației", + "select_error_in_app_install_path": "Noua cale nu poate fi aceeași cu calea de instalare a aplicației", + "select_error_write_permission": "Noua cale nu are permisiuni de scriere", + "select_title": "Selectează directorul de backup" + }, + "hour_interval_one": "{{count}} oră", + "hour_interval_other": "{{count}} ore", + "lastSync": "Ultimul backup", + "maxBackups": { + "label": "Backup-uri maxime", + "unlimited": "Nelimitat" + }, + "minute_interval_one": "{{count}} minut", + "minute_interval_other": "{{count}} minute", + "noSync": "Se așteaptă următorul backup", + "restore": { + "button": "Restaurează din local", + "confirm": { + "content": "Restaurarea din backup-ul local va înlocui datele actuale. Vrei să continui?", + "title": "Confirmă restaurarea" + } + }, + "syncError": "Eroare backup", + "syncStatus": "Stare backup", + "title": "Backup local" + }, + "markdown_export": { + "exclude_citations": { + "help": "Exclude citările și referințele la exportul în Markdown, păstrând doar conținutul principal", + "title": "Exclude citările" + }, + "force_dollar_math": { + "help": "Când este activat, $$ va fi folosit forțat pentru a marca formulele LaTeX la exportul în Markdown. Notă: Această opțiune afectează și toate metodele de export prin Markdown, cum ar fi Notion, Yuque etc.", + "title": "Forțează $$ pentru formulele LaTeX" + }, + "help": "Dacă este furnizată, exporturile vor fi salvate automat în această cale; în caz contrar, va apărea un dialog de salvare.", + "path": "Cale export implicită", + "path_placeholder": "Cale export", + "select": "Selectează", + "show_model_name": { + "help": "Când este activat, numele modelului va fi afișat la exportul în Markdown. Notă: Această opțiune afectează și toate metodele de export prin Markdown, cum ar fi Notion, Yuque etc.", + "title": "Folosește numele modelului la export" + }, + "show_model_provider": { + "help": "Afișează furnizorul modelului (de ex., OpenAI, Gemini) la exportul în Markdown", + "title": "Arată furnizorul modelului" + }, + "standardize_citations": { + "help": "Când este activat, marcatorii de citare vor fi convertiți în format standard de notă de subsol Markdown [^1], iar listele de citare vor fi formatate.", + "title": "Standardizează formatul citării" + }, + "title": "Export Markdown" + }, + "message_title": { + "use_topic_naming": { + "help": "Când este activat, folosește modelul rapid pentru a numi titlul mesajelor exportate. Această setare afectează și toate metodele de export prin Markdown.", + "title": "Folosește modelul rapid pentru a numi titlul mesajului exportat" + } + }, + "minute_interval_one": "{{count}} minut", + "minute_interval_other": "{{count}} minute", + "notion": { + "api_key": "Cheie API Notion", + "api_key_placeholder": "Introdu cheia API Notion", + "check": { + "button": "Verifică", + "empty_api_key": "Cheia API nu este configurată", + "empty_database_id": "ID-ul bazei de date nu este configurat", + "error": "Eroare de conexiune, te rugăm să verifici configurația rețelei și cheia API și ID-ul bazei de date", + "fail": "Conexiune eșuată, te rugăm să verifici rețeaua și cheia API și ID-ul bazei de date", + "success": "Conexiune reușită" + }, + "database_id": "ID bază de date Notion", + "database_id_placeholder": "Introdu ID-ul bazei de date Notion", + "export_reasoning": { + "help": "Când este activat, conținutul exportat va include lanțul de raționament (procesul de gândire).", + "title": "Include lanțul de raționament în export" + }, + "help": "Documentație configurare Notion", + "page_name_key": "Nume câmp titlu pagină", + "page_name_key_placeholder": "Introdu numele câmpului pentru titlul paginii, implicit este Name", + "title": "Setări Notion" + }, + "nutstore": { + "backup": { + "button": "Backup în Nutstore", + "modal": { + "filename": { + "placeholder": "Introdu numele fișierului de backup" + }, + "title": "Backup în Nutstore" + } + }, + "checkConnection": { + "fail": "Conexiunea Nutstore a eșuat", + "name": "Verifică conexiunea", + "success": "Conectat la Nutstore" + }, + "isLogin": "Conectat", + "login": { + "button": "Conectare" + }, + "logout": { + "button": "Deconectare", + "content": "După deconectare, nu vei mai putea face backup în Nutstore sau restaura din Nutstore.", + "title": "Ești sigur că vrei să te deconectezi de la Nutstore?" + }, + "new_folder": { + "button": { + "cancel": "Anulează", + "confirm": "Confirmă", + "label": "Dosar nou" + } + }, + "notLogin": "Neconectat", + "path": { + "label": "Cale stocare Nutstore", + "placeholder": "Introdu calea de stocare Nutstore" + }, + "pathSelector": { + "currentPath": "Cale curentă", + "return": "Înapoi", + "title": "Cale stocare Nutstore" + }, + "restore": { + "button": "Restaurează din Nutstore", + "confirm": { + "content": "Restaurarea din Nutstore va suprascrie datele curente. Vrei să continui?", + "title": "Restaurează din Nutstore" + } + }, + "title": "Configurare Nutstore", + "username": "Nume utilizator Nutstore" + }, + "obsidian": { + "default_vault": "Seif Obsidian implicit", + "default_vault_export_failed": "Export eșuat", + "default_vault_fetch_error": "Nu s-a putut prelua seiful Obsidian", + "default_vault_loading": "Se încarcă seiful Obsidian...", + "default_vault_no_vaults": "Nu s-au găsit seifuri Obsidian", + "default_vault_placeholder": "Te rugăm să selectezi seiful Obsidian implicit", + "title": "Configurare Obsidian" + }, + "s3": { + "accessKeyId": { + "label": "ID cheie de acces", + "placeholder": "ID cheie de acces" + }, + "autoSync": { + "hour": "La fiecare {{count}} oră", + "label": "Sincronizare automată", + "minute": "La fiecare {{count}} minute", + "off": "Oprit" + }, + "backup": { + "button": "Backup acum", + "error": "Backup S3 eșuat: {{message}}", + "manager": { + "button": "Gestionează backup-uri" + }, + "modal": { + "filename": { + "placeholder": "Te rugăm să introduci numele fișierului de backup" + }, + "title": "Backup S3" + }, + "operation": "Operațiune de backup", + "success": "Backup S3 reușit" + }, + "bucket": { + "label": "Bucket", + "placeholder": "Bucket, de ex.: exemplu" + }, + "endpoint": { + "label": "Endpoint API", + "placeholder": "https://s3.example.com" + }, + "manager": { + "close": "Închide", + "columns": { + "actions": "Acțiuni", + "fileName": "Nume fișier", + "modifiedTime": "Ora modificării", + "size": "Dimensiune fișier" + }, + "config": { + "incomplete": "Te rugăm să completezi configurația S3 completă" + }, + "delete": { + "confirm": { + "multiple": "Ești sigur că vrei să ștergi cele {{count}} fișiere de backup selectate? Această acțiune nu poate fi anulată.", + "single": "Ești sigur că vrei să ștergi fișierul de backup \"{{fileName}}\"? Această acțiune nu poate fi anulată.", + "title": "Confirmă ștergerea" + }, + "error": "Nu s-a putut șterge fișierul de backup: {{message}}", + "label": "Șterge", + "selected": "Șterge selectate ({{count}})", + "success": { + "multiple": "S-au șters cu succes {{count}} fișiere de backup", + "single": "Fișier de backup șters cu succes" + } + }, + "files": { + "fetch": { + "error": "Nu s-a putut prelua lista fișierelor de backup: {{message}}" + } + }, + "refresh": "Reîmprospătează", + "restore": "Restaurează", + "select": { + "warning": "Te rugăm să selectezi fișierele de backup de șters" + }, + "title": "Manager fișiere backup S3" + }, + "maxBackups": { + "label": "Backup-uri maxime", + "unlimited": "Nelimitat" + }, + "region": { + "label": "Regiune", + "placeholder": "Regiune, de ex.: us-east-1" + }, + "restore": { + "config": { + "incomplete": "Te rugăm să completezi configurația S3 completă" + }, + "confirm": { + "cancel": "Anulează", + "content": "Restaurarea datelor va suprascrie toate datele curente. Această acțiune nu poate fi anulată. Ești sigur că vrei să continui?", + "ok": "Confirmă restaurarea", + "title": "Confirmă restaurarea datelor" + }, + "error": "Restaurarea datelor a eșuat: {{message}}", + "file": { + "required": "Te rugăm să selectezi fișierul de backup pentru restaurare" + }, + "modal": { + "select": { + "placeholder": "Te rugăm să selectezi fișierul de backup pentru restaurare" + }, + "title": "Restaurare date S3" + }, + "success": "Restaurarea datelor a reușit" + }, + "root": { + "label": "Director backup (Opțional)", + "placeholder": "de ex.: /cherry-studio" + }, + "secretAccessKey": { + "label": "Cheie secretă de acces", + "placeholder": "Cheie secretă de acces" + }, + "skipBackupFile": { + "help": "Când este activat, datele fișierelor vor fi omise în timpul backup-ului, vor fi salvate doar informațiile de configurare, reducând semnificativ dimensiunea fișierului de backup", + "label": "Backup ușor" + }, + "syncStatus": { + "error": "Eroare sincronizare: {{message}}", + "label": "Stare sincronizare", + "lastSync": "Ultima sincronizare: {{time}}", + "noSync": "Nesincronizat" + }, + "title": { + "help": "Servicii de stocare a obiectelor compatibile S3, cum ar fi AWS S3, Cloudflare R2, Aliyun OSS, Tencent COS etc.", + "label": "Stocare compatibilă S3", + "tooltip": "Documentație configurare stocare compatibilă S3" + } + }, + "siyuan": { + "api_url": "URL API SiYuan Note", + "api_url_placeholder": "de ex.: http://127.0.0.1:6806", + "box_id": "ID Box SiYuan Note", + "box_id_placeholder": "Te rugăm să introduci ID-ul Box SiYuan Note", + "check": { + "button": "Verifică", + "empty_config": "Te rugăm să completezi adresa API și tokenul", + "error": "Eroare de conexiune, te rugăm să verifici conexiunea la rețea", + "fail": "Conexiune eșuată, te rugăm să verifici adresa API și tokenul", + "success": "Conexiune reușită", + "title": "Verificare conexiune" + }, + "root_path": "Cale rădăcină SiYuan Note", + "root_path_placeholder": "de ex.: /CherryStudio", + "title": "Configurare SiYuan Note", + "token": { + "help": "Obține token SiYuan Note", + "label": "Token SiYuan Note" + }, + "token_placeholder": "Te rugăm să introduci tokenul SiYuan Note" + }, + "title": "Setări date", + "webdav": { + "autoSync": { + "label": "Backup automat", + "off": "Oprit" + }, + "backup": { + "button": "Backup în WebDAV", + "manager": { + "columns": { + "actions": "Acțiuni", + "fileName": "Nume fișier", + "modifiedTime": "Ora modificării", + "size": "Dimensiune" + }, + "delete": { + "confirm": { + "multiple": "Ești sigur că vrei să ștergi cele {{count}} fișiere de backup selectate? Această acțiune nu poate fi anulată.", + "single": "Ești sigur că vrei să ștergi fișierul de backup \"{{fileName}}\"? Această acțiune nu poate fi anulată.", + "title": "Confirmă ștergerea" + }, + "error": "Ștergerea a eșuat", + "selected": "Șterge selectate", + "success": { + "multiple": "S-au șters cu succes {{count}} fișiere de backup", + "single": "Șters cu succes" + }, + "text": "Șterge" + }, + "fetch": { + "error": "Nu s-au putut obține fișierele de backup" + }, + "refresh": "Reîmprospătează", + "restore": { + "error": "Restaurare eșuată", + "success": "Restaurare reușită, aplicația se va reîmprospăta în scurt timp", + "text": "Restaurează" + }, + "select": { + "files": { + "delete": "Te rugăm să selectezi fișierele de backup de șters" + } + }, + "title": "Gestionare date backup" + }, + "modal": { + "filename": { + "placeholder": "Te rugăm să introduci numele fișierului de backup" + }, + "title": "Backup în WebDAV" + } + }, + "disableStream": { + "help": "Când este activat, încarcă fișierul în memorie înainte de încărcare. Acest lucru poate rezolva probleme de incompatibilitate cu unele servere WebDAV care nu acceptă încărcări fragmentate, dar va crește utilizarea memoriei.", + "title": "Dezactivează încărcarea prin flux" + }, + "host": { + "label": "Gazdă WebDAV", + "placeholder": "http://localhost:8080" + }, + "hour_interval_one": "{{count}} oră", + "hour_interval_other": "{{count}} ore", + "lastSync": "Ultimul backup", + "maxBackups": "Backup-uri maxime", + "minute_interval_one": "{{count}} minut", + "minute_interval_other": "{{count}} minute", + "noSync": "Se așteaptă următorul backup", + "password": "Parolă WebDAV", + "path": { + "label": "Cale WebDAV", + "placeholder": "/backup" + }, + "restore": { + "button": "Restaurează din WebDAV", + "confirm": { + "content": "Restaurarea din WebDAV va suprascrie datele curente. Vrei să continui?", + "title": "Confirmă restaurarea" + }, + "content": "Restaurarea din WebDAV va suprascrie datele curente, continui?", + "title": "Restaurează din WebDAV" + }, + "syncError": "Eroare backup", + "syncStatus": "Stare backup", + "title": "WebDAV", + "user": "Utilizator WebDAV" + }, + "yuque": { + "check": { + "button": "Verifică", + "empty_repo_url": "Te rugăm să introduci mai întâi URL-ul bazei de cunoștințe", + "empty_token": "Te rugăm să introduci mai întâi tokenul Yuque", + "fail": "Verificarea conexiunii Yuque a eșuat", + "success": "Conexiunea Yuque a fost verificată cu succes" + }, + "help": "Obține token Yuque", + "repo_url": "URL Yuque", + "repo_url_placeholder": "https://www.yuque.com/username/xxx", + "title": "Configurare Yuque", + "token": "Token Yuque", + "token_placeholder": "Te rugăm să introduci tokenul Yuque" + } + }, + "developer": { + "enable_developer_mode": "Activează modul dezvoltator", + "help": "După activarea modului dezvoltator, poți folosi funcția de urmărire (trace) pentru a vizualiza fluxul de date în timpul invocării modelului.", + "title": "Mod dezvoltator" + }, + "display": { + "assistant": { + "title": "Setări asistent" + }, + "custom": { + "css": { + "cherrycss": "Obține de la cherrycss.com", + "label": "CSS personalizat", + "placeholder": "/* Pune CSS personalizat aici */" + } + }, + "font": { + "code": "Font cod", + "default": "Implicit", + "global": "Font global", + "select": "Selectează font", + "title": "Setări font" + }, + "navbar": { + "position": { + "label": "Poziție bară de navigare", + "left": "Stânga", + "top": "Sus" + }, + "title": "Setări bară de navigare" + }, + "sidebar": { + "chat": { + "hiddenMessage": "Asistenții sunt funcții de bază, nu se acceptă ascunderea" + }, + "disabled": "Ascunde pictograme", + "empty": "Trage funcția ascunsă din partea stângă aici", + "files": { + "icon": "Arată pictograma Fișiere" + }, + "knowledge": { + "icon": "Arată pictograma Cunoștințe" + }, + "minapp": { + "icon": "Arată pictograma MinApp" + }, + "painting": { + "icon": "Arată pictograma Pictură" + }, + "title": "Setări bară laterală", + "translate": { + "icon": "Arată pictograma Traducere" + }, + "visible": "Arată pictograme" + }, + "title": "Setări afișare", + "topic": { + "title": "Setări subiect" + }, + "zoom": { + "title": "Setări zoom" + } + }, + "font_size": { + "title": "Dimensiune font mesaj" + }, + "general": { + "auto_check_update": { + "title": "Actualizare automată" + }, + "avatar": { + "builtin": "Avatar integrat", + "reset": "Resetează avatarul" + }, + "backup": { + "button": "Backup", + "title": "Backup și recuperare date" + }, + "display": { + "title": "Setări afișare" + }, + "emoji_picker": "Selector emoji", + "image_upload": "Încărcare imagine", + "label": "Setări generale", + "reset": { + "button": "Resetează", + "title": "Resetare date" + }, + "restore": { + "button": "Restaurează" + }, + "spell_check": { + "label": "Verificare ortografică", + "languages": "Folosește verificarea ortografică pentru" + }, + "test_plan": { + "beta_version": "Versiune Beta", + "beta_version_tooltip": "Funcțiile se pot schimba oricând, mai multe bug-uri, actualizare rapidă", + "rc_version": "Versiune Previzualizare (RC)", + "rc_version_tooltip": "Aproape de versiunea stabilă, funcțiile sunt în principiu stabile, puține bug-uri", + "title": "Plan de testare", + "tooltip": "Participă la planul de testare pentru a experimenta mai rapid cele mai recente funcții, dar aduce și mai multe riscuri; te rugăm să faci backup datelor în avans", + "version_channel_not_match": "Comutarea versiunii de previzualizare și test va intra în vigoare după lansarea următoarei versiuni stabile", + "version_options": "Opțiuni versiune" + }, + "title": "Setări generale", + "user_name": { + "label": "Nume utilizator", + "placeholder": "Introdu numele tău" + }, + "view_webdav_settings": "Vezi setările WebDAV" + }, + "groq": { + "title": "Setări Groq" + }, + "hardware_acceleration": { + "confirm": { + "content": "Dezactivarea accelerării hardware necesită repornirea aplicației pentru a intra în vigoare. Vrei să repornești acum?", + "title": "Repornire necesară" + }, + "title": "Dezactivează accelerarea hardware" + }, + "input": { + "auto_translate_with_space": "Tradu rapid cu 3 spații", + "clear": { + "all": "Golește", + "knowledge_base": "Golește bazele de cunoștințe selectate", + "models": "Golește toate modelele" + }, + "show_translate_confirm": "Arată dialogul de confirmare a traducerii", + "target_language": { + "chinese": "Chineză simplificată", + "chinese-traditional": "Chineză tradițională", + "english": "Engleză", + "japanese": "Japoneză", + "label": "Limba țintă", + "russian": "Rusă" + } + }, + "launch": { + "onboot": "Pornește automat la pornirea sistemului", + "title": "Lansare", + "totray": "Minimizează în zona de notificare la pornire" + }, + "math": { + "engine": { + "label": "Motor matematic", + "none": "Niciunul" + }, + "single_dollar": { + "label": "Activează $...$", + "tip": "Randează ecuațiile matematice citate prin semne unice de dolar $...$. Implicit este activat." + }, + "title": "Setări matematice" + }, + "mcp": { + "actions": "Acțiuni", + "active": "Activ", + "addError": "Nu s-a putut adăuga serverul", + "addServer": { + "create": "Creare rapidă", + "importFrom": { + "connectionFailed": "Conexiune eșuată", + "dxt": "Importă pachet DXT", + "dxtFile": "Fișier pachet DXT", + "dxtHelp": "Selectează un fișier .dxt care conține un pachet de server MCP", + "dxtProcessFailed": "Procesarea fișierului DXT a eșuat", + "error": { + "multipleServers": "Nu se poate importa din mai multe servere" + }, + "invalid": "Intrare invalidă, te rugăm să verifici formatul JSON", + "json": "Importă din JSON", + "method": "Metodă import", + "nameExists": "Serverul există deja: {{name}}", + "noDxtFile": "Te rugăm să selectezi un fișier DXT", + "oneServer": "Doar o singură configurație de server MCP la un moment dat", + "placeholder": "Lipește configurația JSON a serverului MCP", + "selectDxtFile": "Selectează fișierul DXT", + "tooltip": "Te rugăm să copiezi JSON-ul de configurare (prioritizând configurațiile\n NPX sau UVX) din pagina de introducere a serverelor MCP și să-l lipești în caseta de intrare." + }, + "label": "Adaugă server" + }, + "addSuccess": "Server adăugat cu succes", + "advancedSettings": "Setări avansate", + "args": "Argumente", + "argsTooltip": "Fiecare argument pe o linie nouă", + "baseUrlTooltip": "URL de bază server la distanță", + "builtinServers": "Servere integrate", + "builtinServersDescriptions": { + "brave_search": "O implementare de server MCP care integrează API-ul Brave Search, oferind funcționalități de căutare web și locală. Necesită configurarea variabilei de mediu BRAVE_API_KEY", + "browser": "Controlează o fereastră Electron headless prin Protocolul Chrome DevTools. Instrumente: deschide URL, execută JS pe o singură linie, resetează sesiunea.", + "didi_mcp": "Server DiDi MCP care oferă servicii de ride-hailing, inclusiv căutare pe hartă, estimare preț, gestionare comenzi și urmărire șofer. Disponibil doar în China continentală. Necesită configurarea variabilei de mediu DIDI_API_KEY", + "dify_knowledge": "Implementarea serverului MCP Dify oferă un API simplu pentru a interacționa cu Dify. Necesită configurarea cheii Dify", + "fetch": "Server MCP pentru preluarea conținutului web de la URL", + "filesystem": "Un server Node.js care implementează Protocolul de Context Model (MCP) pentru operațiuni în sistemul de fișiere. Necesită configurarea directoarelor permise pentru acces.", + "mcp_auto_install": "Instalează automat serviciul MCP (beta)", + "memory": "Implementare de memorie persistentă bazată pe un graf de cunoștințe local. Aceasta permite modelului să rețină informații legate de utilizator între conversații diferite. Necesită configurarea variabilei de mediu MEMORY_FILE_PATH.", + "no": "Fără descriere", + "nowledge_mem": "Necesită aplicația Nowledge Mem rulând local. Păstrează chat-urile AI, instrumentele, notițele, agenții și fișierele în memoria privată de pe computerul tău. Descarcă de la https://mem.nowledge.co/", + "python": "Execută cod Python într-un mediu sandbox securizat. Rulează Python cu Pyodide, suportând majoritatea bibliotecilor standard și pachetelor de calcul științific", + "sequentialthinking": "O implementare de server MCP care oferă instrumente pentru rezolvarea dinamică și reflexivă a problemelor prin procese de gândire structurată" + }, + "command": "Comandă", + "config_description": "Configurează serverele Protocolului de Context Model", + "customRegistryPlaceholder": "Introdu URL registru privat, de ex.: https://npm.company.com", + "deleteError": "Nu s-a putut șterge serverul", + "deleteServer": "Șterge serverul", + "deleteServerConfirm": "Ești sigur că vrei să ștergi acest server?", + "deleteSuccess": "Server șters cu succes", + "dependenciesInstall": "Instalează dependențe", + "dependenciesInstalling": "Se instalează dependențele...", + "description": "Descriere", + "disable": { + "description": "Nu activa funcționalitatea serverului MCP", + "label": "Dezactivează serverul MCP" + }, + "discover": "Descoperă", + "duplicateName": "Un server cu acest nume există deja", + "editJson": "Editează JSON", + "editMcpJson": "Editează configurația MCP", + "editServer": "Editează serverul", + "env": "Variabile de mediu", + "envTooltip": "Format: CHEIE=valoare, una pe linie", + "errors": { + "32000": "Serverul MCP nu a pornit, te rugăm să verifici parametrii conform tutorialului", + "toolNotFound": "Instrumentul {{name}} nu a fost găsit" + }, + "fetch": { + "button": "Preluare servere", + "success": "Serverele MCP au fost preluate cu succes" + }, + "findMore": "Găsește mai multe MCP", + "headers": "Headere", + "headersTooltip": "Headere personalizate pentru cereri HTTP", + "inMemory": "Memorie", + "install": "Instalează", + "installError": "Instalarea dependențelor a eșuat", + "installHelp": "Obține ajutor pentru instalare", + "installSuccess": "Dependențe instalate cu succes", + "jsonFormatError": "Eroare formatare JSON", + "jsonModeHint": "Editează reprezentarea JSON a configurației serverului MCP. Te rugăm să te asiguri că formatul este corect înainte de salvare.", + "jsonSaveError": "Nu s-a putut salva configurația JSON.", + "jsonSaveSuccess": "Configurația JSON a fost salvată.", + "logoUrl": "URL logo", + "logs": "Jurnale", + "longRunning": "Mod rulare lungă", + "longRunningTooltip": "Când este activat, serverul acceptă sarcini de lungă durată. La primirea notificărilor de progres, timpul de expirare va fi resetat, iar timpul maxim de execuție va fi extins la 10 minute.", + "marketplaces": "Piețe", + "missingDependencies": "Lipsește, te rugăm să îl instalezi pentru a continua.", + "more": { + "awesome": "Listă curatoriată servere MCP", + "composio": "Instrumente dezvoltare MCP Composio", + "glama": "Director servere MCP Glama", + "higress": "Server MCP Higress", + "mcpso": "Platformă descoperire servere MCP", + "modelscope": "Server MCP comunitate ModelScope", + "official": "Colecție oficială servere MCP", + "pulsemcp": "Server MCP Pulse", + "smithery": "Instrumente MCP Smithery", + "zhipu": "MCP curatoriat, integrare rapidă" + }, + "name": "Nume", + "newServer": "Server MCP", + "noDescriptionAvailable": "Nicio descriere disponibilă", + "noLogs": "Niciun jurnal încă", + "noServers": "Niciun server configurat", + "not_support": "Model neacceptat", + "npx_list": { + "actions": "Acțiuni", + "description": "Descriere", + "no_packages": "Nu s-au găsit pachete", + "npm": "NPM", + "package_name": "Nume pachet", + "scope_placeholder": "Introdu domeniul npm (de ex. @organizatia-ta)", + "scope_required": "Te rugăm să introduci domeniul npm", + "search": "Caută", + "search_error": "Eroare căutare", + "usage": "Utilizare", + "version": "Versiune" + }, + "oauth": { + "callback": { + "message": "Poți închide această pagină și te poți întoarce la Cherry Studio", + "title": "Autentificare reușită" + } + }, + "prompts": { + "arguments": "Argumente", + "availablePrompts": "Prompturi disponibile", + "genericError": "Eroare obținere prompt", + "loadError": "Eroare obținere prompturi", + "noPromptsAvailable": "Nu există prompturi disponibile", + "requiredField": "Câmp obligatoriu" + }, + "protocolInstallWarning": { + "command": "Comandă pornire", + "message": "Acest MCP a fost instalat dintr-o sursă externă prin protocol. Rularea instrumentelor necunoscute poate dăuna computerului tău.", + "run": "Rulează", + "title": "Rulezi MCP extern?" + }, + "provider": "Furnizor", + "providerPlaceholder": "Nume furnizor", + "providerUrl": "URL furnizor", + "providers": "Furnizori", + "registry": "Registru pachete", + "registryDefault": "Implicit", + "registryTooltip": "Alege registrul pentru instalarea pachetelor pentru a rezolva problemele de rețea cu registrul implicit.", + "requiresConfig": "Necesită configurare", + "resources": { + "availableResources": "Resurse disponibile", + "blob": "Blob", + "blobInvisible": "Blob invizibil", + "genericError": "Eroare achiziție resursă", + "mimeType": "Tip MIME", + "noResourcesAvailable": "Nu există resurse disponibile", + "size": "Dimensiune", + "text": "Text", + "uri": "URI" + }, + "search": { + "placeholder": "Caută servere MCP...", + "tooltip": "Caută servere MCP" + }, + "searchNpx": "Caută MCP", + "serverPlural": "servere", + "serverSingular": "server", + "servers": "Servere MCP", + "sse": "Evenimente trimise de server (sse)", + "startError": "Pornire eșuată", + "stdio": "Intrare/Ieșire standard (stdio)", + "streamableHttp": "HTTP fluxabil (streamableHttp)", + "sync": { + "button": "Sincronizează", + "discoverMcpServers": "Descoperă servere MCP", + "discoverMcpServersDescription": "Vizitează platforma pentru a descoperi servere MCP disponibile", + "error": "Eroare sincronizare servere MCP", + "getToken": "Obține token API", + "getTokenDescription": "Obține tokenul tău personal API din contul tău", + "noServersAvailable": "Nu există servere MCP disponibile", + "selectProvider": "Selectează furnizor:", + "setToken": "Introdu tokenul tău", + "success": "Sincronizare servere MCP reușită", + "title": "Sincronizare servere", + "tokenPlaceholder": "Introdu tokenul API aici", + "tokenRequired": "Tokenul API este obligatoriu", + "unauthorized": "Sincronizare neautorizată" + }, + "system": "Sistem", + "tabs": { + "description": "Descriere", + "general": "General", + "prompts": "Prompturi", + "resources": "Resurse", + "tools": "Instrumente" + }, + "tags": "Etichete", + "tagsPlaceholder": "Introdu etichete", + "timeout": "Expirare", + "timeoutTooltip": "Timpul de expirare în secunde pentru cererile către acest server, implicit este 60 secunde", + "title": "Servere MCP", + "tools": { + "autoApprove": { + "label": "Aprobare automată", + "tooltip": { + "confirm": "Ești sigur că vrei să rulezi acest instrument MCP?", + "disabled": "Instrumentul va necesita aprobare manuală înainte de rulare", + "enabled": "Instrumentul va rula automat fără confirmare", + "howToEnable": "Activează mai întâi instrumentul pentru a folosi aprobarea automată" + } + }, + "availableTools": "Instrumente disponibile", + "enable": "Activează instrumentul", + "inputSchema": { + "enum": { + "allowedValues": "Valori permise" + }, + "label": "Schemă intrare" + }, + "loadError": "Eroare obținere instrumente", + "noToolsAvailable": "Nu există instrumente disponibile", + "run": "Rulează" + }, + "type": "Tip", + "types": { + "inMemory": "În memorie", + "sse": "SSE", + "stdio": "STDIO", + "streamableHttp": "HTTP fluxabil" + }, + "updateError": "Actualizarea serverului a eșuat", + "updateSuccess": "Server actualizat cu succes", + "url": "URL", + "user": "Utilizator" + }, + "messages": { + "divider": { + "label": "Arată divizor între mesaje", + "tooltip": "Nu se aplică mesajelor stil bulă" + }, + "grid_columns": "Coloane afișare grilă mesaje", + "grid_popover_trigger": { + "click": "Fă clic pentru a afișa", + "hover": "Plasează cursorul pentru a afișa", + "label": "Declanșator detaliu grilă" + }, + "input": { + "confirm_delete_message": "Confirmă înainte de ștergerea mesajelor", + "confirm_regenerate_message": "Confirmă înainte de regenerarea mesajelor", + "enable_quick_triggers": "Activează declanșatoarele / și @", + "paste_long_text_as_file": "Lipește text lung ca fișier", + "paste_long_text_threshold": "Lungime lipire text lung", + "send_shortcuts": "Comenzi rapide trimitere", + "show_estimated_tokens": "Arată tokeni estimați", + "title": "Setări intrare" + }, + "markdown_rendering_input_message": "Randare Markdown mesaj intrare", + "metrics": "{{time_first_token_millsec}}ms până la primul token | {{token_speed}} tok/sec", + "model": { + "title": "Setări model" + }, + "navigation": { + "anchor": "Ancoră mesaj", + "buttons": "Butoane navigare", + "label": "Bară navigare", + "none": "Niciunul" + }, + "prompt": "Arată prompt", + "show_message_outline": "Arată contur mesaj", + "title": "Setări mesaje", + "use_serif_font": "Folosește font serif" + }, + "mineru": { + "api_key": "Mineru oferă acum o cotă zilnică gratuită de 500 de pagini și nu este nevoie să introduci o cheie." + }, + "miniapps": { + "cache_change_notice": "Modificările vor intra în vigoare când numărul de mini-aplicații deschise atinge valoarea setată", + "cache_description": "Setează numărul maxim de mini-aplicații active de păstrat în memorie", + "cache_settings": "Setări cache", + "cache_title": "Limită cache mini-aplicații", + "custom": { + "conflicting_ids": "ID-uri conflictuale cu aplicațiile implicite: {{ids}}", + "duplicate_ids": "ID-uri duplicate găsite: {{ids}}", + "edit_description": "Editează configurația mini-aplicației personalizate aici. Fiecare aplicație ar trebui să includă câmpurile id, name, url și logo.", + "edit_title": "Editează mini-aplicație personalizată", + "id": "ID", + "id_error": "ID-ul este obligatoriu.", + "id_placeholder": "Introdu ID", + "logo": "Logo", + "logo_file": "Încarcă fișier logo", + "logo_upload_button": "Încarcă", + "logo_upload_error": "Încărcarea logo-ului a eșuat.", + "logo_upload_label": "Încarcă logo", + "logo_upload_success": "Logo încărcat cu succes.", + "logo_url": "URL logo", + "logo_url_label": "URL logo", + "logo_url_placeholder": "Introdu URL logo", + "name": "Nume", + "name_error": "Numele este obligatoriu.", + "name_placeholder": "Introdu nume", + "placeholder": "Introdu configurația mini-aplicației personalizate (format JSON)", + "remove_error": "Eliminarea mini-aplicației personalizate a eșuat.", + "remove_success": "Mini-aplicația personalizată a fost eliminată cu succes.", + "save": "Salvează", + "save_error": "Salvarea mini-aplicației personalizate a eșuat.", + "save_success": "Mini-aplicația personalizată a fost salvată cu succes.", + "title": "Personalizat", + "url": "URL", + "url_error": "URL-ul este obligatoriu.", + "url_placeholder": "Introdu URL" + }, + "disabled": "Mini-aplicații ascunse", + "display_title": "Setări afișare mini-aplicații", + "empty": "Trage mini-aplicațiile din stânga pentru a le ascunde", + "open_link_external": { + "title": "Deschide linkurile de fereastră nouă în browser" + }, + "reset_tooltip": "Resetează la implicit", + "sidebar_description": "Arată mini-aplicațiile active în bara laterală", + "sidebar_title": "Afișare mini-aplicații active în bara laterală", + "title": "Setări mini-aplicații", + "visible": "Mini-aplicații vizibile" + }, + "model": "Model implicit", + "models": { + "add": { + "add_model": "Adaugă model", + "batch_add_models": "Adaugă modele în lot", + "endpoint_type": { + "label": "Tip endpoint", + "placeholder": "Selectează tip endpoint", + "required": "Te rugăm să selectezi un tip de endpoint", + "tooltip": "Selectează formatul tipului de endpoint API" + }, + "group_name": { + "label": "Nume grup", + "placeholder": "Opțional de ex. ChatGPT", + "tooltip": "Opțional de ex. ChatGPT" + }, + "model_id": { + "label": "ID model", + "placeholder": "Obligatoriu de ex. gpt-3.5-turbo", + "select": { + "placeholder": "Selectează model" + }, + "tooltip": "Exemplu: gpt-3.5-turbo" + }, + "model_name": { + "label": "Nume model", + "placeholder": "Opțional de ex. GPT-4", + "tooltip": "Opțional de ex. GPT-4" + }, + "supported_text_delta": { + "label": "Suportă ieșire text incrementală", + "tooltip": "Modelul returnează text incremental, mai degrabă decât tot odată. Activat implicit, dacă modelul nu acceptă acest lucru, te rugăm să dezactivezi această opțiune" + } + }, + "api_key": "Cheie API", + "base_url": "URL de bază", + "check": { + "all": "Toate", + "all_models_passed": "Verificarea tuturor modelelor a trecut", + "button_caption": "Verificare sănătate", + "disabled": "Dezactivat", + "disclaimer": "Verificarea sănătății necesită trimiterea de cereri, te rugăm să o folosești cu precauție. Modelele care taxează pe cerere pot genera costuri suplimentare, te rugăm să îți asumi responsabilitatea.", + "enable_concurrent": "Concurent", + "enabled": "Activat", + "failed": "Eșuat", + "keys_status_count": "Reușite: {{count_passed}} chei, eșuate: {{count_failed}} chei", + "model_status_failed": "{{count}} modele complet inaccesibile", + "model_status_partial": "{{count}} modele au avut chei inaccesibile", + "model_status_passed": "{{count}} modele au trecut verificările de sănătate", + "model_status_summary": "{{provider}}: {{summary}}", + "no_api_keys": "Nu s-au găsit chei API, te rugăm să adaugi mai întâi chei API.", + "no_results": "Niciun rezultat", + "passed": "Reușit", + "select_api_key": "Selectează cheia API de utilizat:", + "single": "Singur", + "start": "Start", + "timeout": "Expirare", + "title": "Verificare sănătate model", + "use_all_keys": "Cheie(i)" + }, + "default_assistant_model": "Model asistent implicit", + "default_assistant_model_description": "Model folosit la crearea unui nou asistent; dacă asistentul nu este setat, va fi folosit acest model", + "empty": "Nu s-au găsit modele", + "manage": { + "add_listed": { + "confirm": "Ești sigur că vrei să adaugi toate modelele la listă?", + "label": "Adaugă modele la listă" + }, + "add_whole_group": "Adaugă întregul grup", + "refetch_list": "Reîmprospătează lista modelelor", + "remove_listed": "Elimină modelele din listă", + "remove_model": "Elimină modelul", + "remove_whole_group": "Elimină întregul grup" + }, + "provider_id": "ID furnizor", + "provider_key_add_confirm": "Vrei să adaugi cheia API pentru {{provider}}?", + "provider_key_add_failed_by_empty_data": "Adăugarea cheii API a furnizorului a eșuat, datele sunt goale", + "provider_key_add_failed_by_invalid_data": "Adăugarea cheii API a furnizorului a eșuat, eroare format date", + "provider_key_added": "S-a adăugat cu succes cheia API pentru {{provider}}", + "provider_key_already_exists": "{{provider}} are deja o cheie API ({{existingKey}}). Nu o adăuga din nou.", + "provider_key_confirm_title": "Adaugă cheie API furnizor", + "provider_key_no_change": "Cheia API pentru {{provider}} nu s-a schimbat", + "provider_key_overridden": "S-a actualizat cu succes cheia API pentru {{provider}}", + "provider_key_override_confirm": "{{provider}} are deja o cheie API ({{existingKey}}). Vrei să o suprascrii cu noua cheie ({{newKey}})?", + "provider_name": "Nume furnizor", + "quick_assistant_default_tag": "Implicit", + "quick_assistant_model": "Model asistent rapid", + "quick_assistant_selection": "Selectează asistent", + "quick_model": { + "description": "Model folosit pentru sarcini simple, cum ar fi numirea subiectelor și extragerea cuvintelor cheie", + "label": "Model rapid", + "setting_title": "Configurare model rapid", + "tooltip": "Se recomandă alegerea unui model ușor și nu se recomandă alegerea unui model de gândire." + }, + "topic_naming": { + "auto": "Numire automată subiect", + "label": "Numire subiect", + "prompt": "Prompt numire subiect" + }, + "translate_model": "Model traducere", + "translate_model_description": "Model folosit pentru serviciul de traducere", + "translate_model_prompt_message": "Te rugăm să introduci promptul modelului de traducere", + "translate_model_prompt_title": "Prompt model traducere", + "use_assistant": "Folosește asistent", + "use_model": "Model implicit" + }, + "moresetting": { + "check": { + "confirm": "Confirmă selecția", + "warn": "Te rugăm să fii precaut când selectezi această opțiune. Selecția incorectă poate cauza funcționarea defectuoasă a modelului!" + }, + "label": "Mai multe setări", + "warn": "Avertisment de risc" + }, + "no_provider_selected": "Furnizor neselectat", + "notification": { + "assistant": "Mesaj asistent", + "backup": "Mesaj backup", + "knowledge_embed": "Mesaj bază de cunoștințe", + "title": "Setări notificări" + }, + "openai": { + "service_tier": { + "auto": "auto", + "default": "implicit", + "flex": "flex", + "on_demand": "la cerere", + "priority": "prioritate", + "tip": "Specifică nivelul de latență de utilizat pentru procesarea cererii", + "title": "Nivel serviciu" + }, + "stream_options": { + "include_usage": { + "tip": "Dacă utilizarea tokenilor este inclusă (aplicabil doar API-ului OpenAI Chat Completions)", + "title": "Include utilizare" + } + }, + "summary_text_mode": { + "auto": "auto", + "concise": "concis", + "detailed": "detaliat", + "off": "oprit", + "tip": "Un rezumat al raționamentului efectuat de model", + "title": "Mod rezumat" + }, + "title": "Setări OpenAI", + "verbosity": { + "high": "Ridicat", + "low": "Scăzut", + "medium": "Mediu", + "tip": "Controlează nivelul de detaliu în ieșirea modelului", + "title": "Verbozitate" + } + }, + "privacy": { + "enable_privacy_mode": "Raportare anonimă a erorilor și statisticilor", + "title": "Setări confidențialitate" + }, + "provider": { + "add": { + "name": { + "label": "Nume furnizor", + "placeholder": "Exemplu: OpenAI" + }, + "title": "Adaugă furnizor", + "type": "Tip furnizor" + }, + "anthropic": { + "apikey": "Cheie API", + "auth_failed": "Autentificarea Anthropic a eșuat", + "auth_method": "Metodă de autentificare", + "auth_success": "Autentificare OAuth Anthropic reușită", + "authenticated": "Verificat", + "authenticating": "Se autentifică", + "cancel": "Anulează", + "code_error": "Cod de autorizare invalid, te rugăm să încerci din nou", + "code_placeholder": "Te rugăm să introduci codul de autorizare afișat în browser", + "code_required": "Codul de autorizare nu poate fi gol", + "description": "Autentificare OAuth", + "description_detail": "Trebuie să te abonezi la Claude Pro sau o versiune superioară pentru a folosi această metodă de autentificare", + "enter_auth_code": "Cod de autorizare", + "logout": "Deconectare", + "logout_failed": "Deconectarea a eșuat, te rugăm să încerci din nou", + "logout_success": "Te-ai deconectat cu succes de la Anthropic", + "oauth": "Web OAuth", + "start_auth": "Începe autorizarea", + "submit_code": "Finalizează conectarea" + }, + "anthropic_api_host": "Gazdă API Anthropic", + "anthropic_api_host_preview": "Previzualizare Anthropic: {{url}}", + "anthropic_api_host_tooltip": "Folosește doar când furnizorul oferă un URL de bază compatibil cu Claude.", + "api": { + "key": { + "check": { + "latency": "Latență" + }, + "error": { + "duplicate": "Cheia API există deja", + "empty": "Cheia API nu poate fi goală" + }, + "list": { + "open": "Deschide interfața de gestionare", + "title": "Gestionare chei API" + }, + "new_key": { + "placeholder": "Introdu una sau mai multe chei" + } + }, + "options": { + "array_content": { + "help": "Furnizorul acceptă ca câmpul content al mesajului să fie de tip array?", + "label": "Acceptă conținut mesaj în format array" + }, + "developer_role": { + "help": "Furnizorul acceptă mesaje cu rolul: \"developer\"?", + "label": "Suportă mesaj dezvoltator" + }, + "enable_thinking": { + "help": "Furnizorul acceptă controlul raționamentului modelelor precum Qwen3 prin parametrul enable_thinking?", + "label": "Suportă enable_thinking" + }, + "label": "Setări API", + "service_tier": { + "help": "Dacă furnizorul acceptă configurarea parametrului service_tier. Când este activat, acest parametru poate fi ajustat în setările nivelului de serviciu de pe pagina de chat. (Doar modele OpenAI)", + "label": "Suportă service_tier" + }, + "stream_options": { + "help": "Furnizorul acceptă parametrul stream_options?", + "label": "Suportă stream_options" + }, + "verbosity": { + "help": "Dacă furnizorul acceptă parametrul verbosity", + "label": "Suportă verbosity" + } + }, + "url": { + "preview": "Previzualizare: {{url}}", + "reset": "Resetează", + "tip": "Adaugă # la final pentru a dezactiva versiunea API adăugată automat." + } + }, + "api_host": "Gazdă API", + "api_host_no_valid": "Adresa API este invalidă", + "api_host_preview": "Previzualizare: {{url}}", + "api_host_tooltip": "Suprascrie doar când furnizorul tău necesită un endpoint personalizat compatibil cu OpenAI.", + "api_key": { + "label": "Cheie API", + "tip": "Folosește virgule pentru a separa mai multe chei" + }, + "api_version": "Versiune API", + "aws-bedrock": { + "access_key_id": "ID cheie acces AWS", + "access_key_id_help": "ID-ul tău de cheie de acces AWS pentru accesarea serviciilor AWS Bedrock", + "api_key": "Cheie API Bedrock", + "api_key_help": "Cheia ta API AWS Bedrock pentru autentificare", + "auth_type": "Tip autentificare", + "auth_type_api_key": "Cheie API Bedrock", + "auth_type_help": "Alege între credențiale IAM sau autentificare cu cheie API Bedrock", + "auth_type_iam": "Credențiale IAM", + "description": "AWS Bedrock este serviciul de modele de fundație complet gestionat de Amazon care acceptă diverse modele lingvistice mari avansate", + "region": "Regiune AWS", + "region_help": "Regiunea serviciului tău AWS, de ex., us-east-1", + "secret_access_key": "Cheie secretă acces AWS", + "secret_access_key_help": "Cheia ta secretă de acces AWS, te rugăm să o păstrezi în siguranță", + "title": "Configurare AWS Bedrock" + }, + "azure": { + "apiversion": { + "tip": "Versiunea API a Azure OpenAI, dacă dorești să folosești API-ul de Răspuns, te rugăm să introduci versiunea v1" + } + }, + "basic_auth": { + "label": "Autentificare HTTP", + "password": { + "label": "Parolă", + "tip": "Introdu parola" + }, + "tip": "Aplicabil instanțelor implementate la distanță (vezi documentația). Momentan, doar schema Basic (RFC 7617) este acceptată.", + "user_name": { + "label": "Nume utilizator", + "tip": "Lasă gol pentru a dezactiva" + } + }, + "bills": "Facturi taxe", + "charge": "Reîncărcare sold", + "check": "Verifică", + "check_all_keys": "Verifică toate cheile", + "check_multiple_keys": "Verifică chei API multiple", + "copilot": { + "auth_failed": "Autentificarea Github Copilot a eșuat.", + "auth_success": "Autentificarea GitHub Copilot a reușit.", + "auth_success_title": "Certificare reușită.", + "code_copied": "Codul de autorizare copiat automat în clipboard", + "code_failed": "Obținerea Codului Dispozitivului a eșuat, te rugăm să încerci din nou.", + "code_generated_desc": "Te rugăm să copiezi codul dispozitivului în linkul de browser de mai jos.", + "code_generated_title": "Obține Cod Dispozitiv", + "connect": "Conectează la Github", + "custom_headers": "Antet cerere personalizat", + "description": "Contul tău GitHub trebuie să fie abonat la Copilot.", + "description_detail": "GitHub Copilot este un asistent de cod bazat pe AI care necesită un abonament GitHub Copilot valid pentru a fi utilizat", + "expand": "Extinde", + "headers_description": "Antete cerere personalizate (format JSON)", + "invalid_json": "Eroare format JSON", + "login": "Conectare la Github", + "logout": "Ieșire GitHub", + "logout_failed": "Ieșirea a eșuat, te rugăm să încerci din nou.", + "logout_success": "Te-ai deconectat cu succes.", + "model_setting": "Setări model", + "open_verification_first": "Te rugăm să faci clic pe linkul de mai sus pentru a accesa pagina de verificare.", + "open_verification_page": "Deschide pagina de autorizare", + "rate_limit": "Limitare rată", + "start_auth": "Începe autorizarea", + "step_authorize": "Deschide pagina de autorizare", + "step_authorize_desc": "Completează autorizarea pe GitHub", + "step_authorize_detail": "Fă clic pe butonul de mai jos pentru a deschide pagina de autorizare GitHub, apoi introdu codul de autorizare copiat", + "step_connect": "Finalizează conexiunea", + "step_connect_desc": "Confirmă conexiunea la GitHub", + "step_connect_detail": "După finalizarea autorizării pe pagina GitHub, fă clic pe acest buton pentru a finaliza conexiunea", + "step_copy_code": "Copiază codul de autorizare", + "step_copy_code_desc": "Copiază codul de autorizare al dispozitivului", + "step_copy_code_detail": "Codul de autorizare a fost copiat automat, îl poți copia și manual", + "step_get_code": "Obține codul de autorizare", + "step_get_code_desc": "Generează codul de autorizare al dispozitivului" + }, + "delete": { + "content": "Ești sigur că vrei să ștergi acest furnizor?", + "title": "Șterge furnizor" + }, + "dmxapi": { + "select_platform": "Selectează platforma" + }, + "docs_check": "Verifică", + "docs_more_details": "pentru mai multe detalii", + "get_api_key": "Obține cheie API", + "misc": "Altele", + "no_models_for_check": "Nu există modele disponibile pentru verificare (de ex. modele chat)", + "not_checked": "Neverificat", + "notes": { + "markdown_editor_default_value": "Zonă previzualizare", + "placeholder": "Introdu conținut Markdown...", + "title": "Note model" + }, + "oauth": { + "button": "Conectare cu {{provider}}", + "description": "Acest serviciu este furnizat de {{provider}}", + "error": "Autentificare eșuată", + "official_website": "Site oficial" + }, + "openai": { + "alert": "Furnizorul OpenAI nu mai acceptă metodele vechi de apelare. Dacă folosești un API terț, te rugăm să creezi un furnizor de servicii nou." + }, + "remove_duplicate_keys": "Elimină cheile duplicate", + "remove_invalid_keys": "Elimină cheile invalide", + "search": "Caută furnizori...", + "search_placeholder": "Caută id sau nume model", + "title": "Furnizor model", + "vertex_ai": { + "api_host_help": "Gazda API pentru Vertex AI, nerecomandat de completat, aplicabil în general pentru reverse proxy", + "documentation": "Vezi documentația oficială pentru mai multe detalii de configurare:", + "learn_more": "Află mai multe", + "location": "Locație", + "location_help": "Locația serviciului Vertex AI, de ex., us-central1", + "project_id": "ID Proiect", + "project_id_help": "ID-ul tău de proiect Google Cloud", + "project_id_placeholder": "id-ul-tau-proiect-google-cloud", + "service_account": { + "auth_success": "Cont de serviciu autentificat cu succes", + "client_email": "E-mail client", + "client_email_help": "Câmpul client_email din fișierul cheie JSON descărcat din Google Cloud Console", + "client_email_placeholder": "Introdu e-mailul clientului Contului de Serviciu", + "description": "Folosește Contul de Serviciu pentru autentificare, potrivit pentru mediile unde ADC nu este disponibil", + "incomplete_config": "Te rugăm să finalizezi mai întâi configurarea Contului de Serviciu", + "private_key": "Cheie privată", + "private_key_help": "Câmpul private_key din fișierul cheie JSON descărcat din Google Cloud Console", + "private_key_placeholder": "Introdu cheia privată a Contului de Serviciu", + "title": "Configurare Cont de Serviciu" + } + } + }, + "proxy": { + "address": "Adresă proxy", + "bypass": "Reguli de ocolire", + "mode": { + "custom": "Proxy personalizat", + "none": "Fără proxy", + "system": "Proxy sistem", + "title": "Mod proxy" + }, + "tip": "Acceptă potrivirea cu wildcard (*.test.com, 192.168.0.0/16)" + }, + "quickAssistant": { + "click_tray_to_show": "Fă clic pe pictograma din zona de notificare pentru a începe", + "enable_quick_assistant": "Activează Asistentul rapid", + "read_clipboard_at_startup": "Citește clipboardul la pornire", + "title": "Asistent rapid", + "use_shortcut_to_show": "Clic dreapta pe pictograma din zona de notificare sau folosește comenzile rapide pentru a începe" + }, + "quickPanel": { + "back": "Înapoi", + "close": "Închide", + "confirm": "Confirmă", + "forward": "Înainte", + "multiple": "Selecție multiplă", + "noResult": "Niciun rezultat găsit", + "page": "Pagină", + "select": "Selectează", + "title": "Meniu rapid" + }, + "quickPhrase": { + "add": "Adaugă expresie", + "assistant": "Expresii asistent", + "contentLabel": "Conținut", + "contentPlaceholder": "Te rugăm să introduci conținutul expresiei, poți folosi variabile și poți apăsa Tab pentru a localiza rapid variabila de modificat. De exemplu: \nAjută-mă să planific o rută de la ${from} la ${to} și trimite-o la ${email}.", + "delete": "Șterge expresia", + "deleteConfirm": "Expresia nu poate fi recuperată după ștergere, continui?", + "edit": "Editează expresia", + "global": "Expresii globale", + "locationLabel": "Adaugă locație", + "title": "Expresii rapide", + "titleLabel": "Titlu", + "titlePlaceholder": "Te rugăm să introduci titlul expresiei" + }, + "shortcuts": { + "action": "Acțiune", + "actions": "operațiune", + "clear_shortcut": "Șterge comanda rapidă", + "clear_topic": "Șterge mesajele", + "copy_last_message": "Copiază ultimul mesaj", + "edit_last_user_message": "Editează ultimul mesaj al utilizatorului", + "enabled": "Activează", + "exit_fullscreen": "Ieși din ecran complet", + "label": "Tastă", + "mini_window": "Asistent rapid", + "new_topic": "Subiect nou", + "press_shortcut": "Apasă comanda rapidă", + "rename_topic": "Redenumește subiectul", + "reset_defaults": "Resetează la implicite", + "reset_defaults_confirm": "Ești sigur că vrei să resetezi toate comenzile rapide?", + "reset_to_default": "Resetează la implicit", + "search_message": "Caută mesaj", + "search_message_in_chat": "Caută mesaj în chat-ul curent", + "selection_assistant_select_text": "Asistent de selecție: Selectează text", + "selection_assistant_toggle": "Comută Asistentul de selecție", + "show_app": "Arată/Ascunde aplicația", + "show_settings": "Deschide setările", + "title": "Comenzi rapide de la tastatură", + "toggle_new_context": "Șterge contextul", + "toggle_show_assistants": "Comută asistenții", + "toggle_show_topics": "Comută subiectele", + "zoom_in": "Mărește", + "zoom_out": "Micșorează", + "zoom_reset": "Resetează zoom-ul" + }, + "theme": { + "color_primary": "Culoare primară", + "dark": "Întunecat", + "light": "Luminos", + "system": "Sistem", + "title": "Temă", + "window": { + "style": { + "opaque": "Fereastră opacă", + "title": "Stil fereastră", + "transparent": "Fereastră transparentă" + } + } + }, + "title": "Setări", + "tool": { + "ocr": { + "common": { + "langs": "Limbi acceptate" + }, + "error": { + "not_system": "OCR-ul de sistem acceptă doar Windows și MacOS" + }, + "image": { + "error": { + "provider_not_found": "Furnizorul nu există" + }, + "system": { + "no_need_configure": "MacOS nu necesită configurare" + }, + "title": "Imagine" + }, + "image_provider": "Furnizor serviciu OCR", + "paddleocr": { + "aistudio_access_token": "Token de acces Comunitatea AI Studio", + "aistudio_url_label": "Comunitatea AI Studio", + "api_url": "URL API", + "serving_doc_url_label": "Documentație servire PaddleOCR", + "tip": "Poți consulta documentația oficială PaddleOCR pentru a implementa un serviciu local sau poți implementa un serviciu cloud pe Comunitatea PaddlePaddle AI Studio. Pentru ultimul caz, te rugăm să furnizezi tokenul de acces al Comunității AI Studio." + }, + "system": { + "win": { + "langs_tooltip": "Dependent de Windows pentru a furniza servicii, trebuie să descarci pachete lingvistice în sistem pentru a suporta limbile relevante." + } + }, + "tesseract": { + "langs_tooltip": "Citește documentația pentru a afla ce limbi personalizate sunt acceptate" + }, + "title": "Serviciu OCR" + }, + "preprocess": { + "provider": "Furnizor procesare documente", + "provider_placeholder": "Alege un furnizor de procesare documente", + "title": "Procesare documente", + "tooltip": "În Setări -> Instrumente, setează un furnizor de servicii de procesare a documentelor. Procesarea documentelor poate îmbunătăți eficient performanța de recuperare a documentelor cu format complex și a documentelor scanate." + }, + "title": "Alte setări", + "websearch": { + "api_key_required": { + "content": "{{provider}} necesită o cheie API pentru a funcționa. Dorești să o configurezi acum?", + "ok": "Configurează", + "title": "Cheie API necesară" + }, + "api_providers": "Furnizori API", + "apikey": "Cheie API", + "blacklist": "Listă neagră", + "blacklist_description": "Rezultatele de pe următoarele site-uri web nu vor apărea în rezultatele căutării", + "blacklist_tooltip": "Te rugăm să folosești următorul format (separate prin linie nouă)\nPotrivire model: *://*.exemplu.com/*\nExpresie regulată: /exemplu\\.(net|org)/", + "check": "Verifică", + "check_failed": "Verificare eșuată", + "check_success": "Verificare reușită", + "compression": { + "cutoff": { + "limit": { + "label": "Limită trunchiere", + "placeholder": "Introdu lungimea", + "tooltip": "Limitează lungimea conținutului rezultatelor căutării, conținutul care depășește limita va fi trunchiat (de ex., 2000 caractere)" + }, + "unit": { + "char": "Caractere", + "token": "Token" + } + }, + "error": { + "rag_failed": "RAG eșuat" + }, + "info": { + "dimensions_auto_success": "Dimensiuni obținute automat cu succes, dimensiuni: {{dimensions}}" + }, + "method": { + "cutoff": "Trunchiere", + "label": "Metodă compresie", + "none": "Niciuna", + "rag": "RAG" + }, + "rag": { + "document_count": { + "label": "Număr fragmente document", + "tooltip": "Numărul așteptat de fragmente de document de extras din fiecare rezultat al căutării; numărul total real de fragmente extrase este această valoare înmulțită cu numărul de rezultate ale căutării." + } + }, + "title": "Compresie rezultate căutare" + }, + "content_limit": "Limită lungime conținut", + "content_limit_tooltip": "Limitează lungimea conținutului rezultatelor căutării; conținutul care depășește limita va fi trunchiat.", + "default_provider": "Furnizor implicit", + "free": "Gratuit", + "is_default": "Implicit", + "local_provider": { + "hint": "Conectează-te la site pentru a obține rezultate mai bune ale căutării și pentru a personaliza setările de căutare.", + "open_settings": "Deschide setările {{provider}}", + "settings": "Setări căutare locală" + }, + "local_providers": "Furnizori locali", + "no_provider_selected": "Te rugăm să selectezi un furnizor de servicii de căutare înainte de a verifica.", + "overwrite": "Suprascrie serviciul de căutare", + "overwrite_tooltip": "Forțează utilizarea serviciului de căutare în loc de LLM", + "search_max_result": { + "label": "Număr de rezultate căutare", + "tooltip": "Când compresia rezultatelor căutării este dezactivată, numărul de rezultate poate fi prea mare, ceea ce poate duce la tokeni insuficienți" + }, + "search_provider": "Furnizor serviciu căutare", + "search_provider_placeholder": "Alege un furnizor de servicii de căutare.", + "search_with_time": "Caută cu date incluse", + "set_as_default": "Setează ca implicit", + "subscribe": "Abonare listă neagră", + "subscribe_add": "Adaugă abonament", + "subscribe_add_failed": "Adăugarea sursei fluxului a eșuat", + "subscribe_add_success": "Flux de abonament adăugat cu succes!", + "subscribe_delete": "Șterge", + "subscribe_name": { + "label": "Nume alternativ", + "placeholder": "Nume alternativ folosit când fluxul de abonament descărcat nu are nume." + }, + "subscribe_update": "Actualizează", + "subscribe_update_failed": "Actualizarea sursei abonamentului a eșuat", + "subscribe_update_success": "Sursa abonamentului a fost actualizată cu succes", + "subscribe_url": "Url abonament", + "tavily": { + "api_key": { + "label": "Cheie API Tavily", + "placeholder": "Introdu cheia API Tavily" + }, + "description": "Tavily este un motor de căutare adaptat pentru agenți AI, oferind rezultate în timp real, precise, sugestii inteligente de interogare și capacități de cercetare aprofundată.", + "title": "Tavily" + }, + "title": "Căutare web", + "url_invalid": "S-a introdus un URL invalid", + "url_required": "Te rugăm să introduci un URL" + } + }, + "topic": { + "pin_to_top": "Fixează subiectele sus", + "position": { + "label": "Poziție subiect", + "left": "Stânga", + "right": "Dreapta" + }, + "show": { + "time": "Arată ora subiectului" + } + }, + "translate": { + "custom": { + "delete": { + "description": "Ești sigur că vrei să ștergi?", + "title": "Șterge limbă personalizată" + }, + "error": { + "add": "Adăugarea a eșuat", + "delete": "Ștergerea a eșuat", + "langCode": { + "builtin": "Limba are suport integrat", + "empty": "Codul limbii este gol", + "exists": "Limba există deja", + "invalid": "Cod limbă invalid" + }, + "update": "Actualizarea a eșuat", + "value": { + "empty": "Numele limbii nu poate fi gol", + "too_long": "Numele limbii este prea lung" + } + }, + "langCode": { + "help": "Format [limbă+regiune], [2-3 litere mici]-[2-3 litere mici]", + "label": "Cod limbă", + "placeholder": "en-us" + }, + "success": { + "add": "Adăugat cu succes", + "delete": "Șters cu succes", + "update": "Actualizare reușită" + }, + "table": { + "action": { + "title": "Operațiune" + } + }, + "value": { + "help": "1~32 caractere", + "label": "Nume limbă", + "placeholder": "Engleză" + } + }, + "prompt": "Prompt traducere", + "title": "Setări traducere" + }, + "tray": { + "onclose": "Minimizează în zona de notificare la închidere", + "show": "Arată pictograma în zona de notificare", + "title": "Zonă de notificare" + }, + "zoom": { + "reset": "Resetează", + "title": "Zoom pagină" + } + }, + "title": { + "apps": "Aplicații", + "code": "Cod", + "files": "Fișiere", + "home": "Acasă", + "knowledge": "Bază de cunoștințe", + "launchpad": "Launchpad", + "mcp-servers": "Servere MCP", + "memories": "Amintiri", + "notes": "Notițe", + "paintings": "Picturi", + "settings": "Setări", + "store": "Bibliotecă asistenți", + "translate": "Traducere" + }, + "trace": { + "backList": "Înapoi la listă", + "edasSupport": "Susținut de Alibaba Cloud EDAS", + "endTime": "Timp final", + "inputs": "Intrări", + "label": "Lanț de apelare", + "name": "Nume nod", + "noTraceList": "Nu s-au găsit informații de urmărire", + "outputs": "Ieșiri", + "parentId": "ID părinte", + "spanDetail": "Detalii interval", + "spendTime": "Timp petrecut", + "startTime": "Timp de început", + "tag": "Etichetă", + "tokenUsage": "Utilizare token", + "traceWindow": "Fereastră lanț de apelare" + }, + "translate": { + "alter_language": "Limbă alternativă", + "any": { + "language": "Orice limbă" + }, + "button": { + "translate": "Tradu" + }, + "close": "Închide", + "closed": "Traducere închisă", + "complete": "Traducere finalizată", + "confirm": { + "content": "Traducerea va înlocui textul original, continui?", + "title": "Confirmare traducere" + }, + "copied": "Conținutul traducerii copiat", + "custom": { + "label": "Limbă personalizată" + }, + "detect": { + "method": { + "algo": { + "label": "algoritm", + "tip": "Folosește biblioteca franc pentru detectarea limbii" + }, + "auto": { + "label": "Automat", + "tip": "Selectează automat metoda de detectare potrivită" + }, + "label": "Metodă de detectare automată", + "llm": { + "tip": "Folosirea modelului rapid pentru detectarea limbii consumă mai puțini tokeni." + }, + "placeholder": "Selectează metoda de detectare automată", + "tip": "Metoda folosită la detectarea automată a limbii de intrare" + } + }, + "detected": { + "language": "Detectare automată" + }, + "empty": "Conținutul traducerii este gol", + "error": { + "chat_qwen_mt": "Modelul Qwen MT nu poate fi folosit în chat. Te rugăm să mergi la pagina de traducere.", + "detect": { + "qwen_mt": "Modelul QwenMT nu poate fi folosit pentru detectarea limbii", + "unknown": "Limbă necunoscută detectată", + "update_setting": "Setarea a eșuat" + }, + "empty": "Rezultatul traducerii este un conținut gol", + "failed": "Traducerea a eșuat", + "invalid_source": "Limbă sursă invalidă", + "not_configured": "Modelul de traducere nu este configurat", + "not_supported": "Limbă neacceptată {{language}}", + "unknown": "A apărut o eroare necunoscută în timpul traducerii" + }, + "exchange": { + "label": "Schimbă limbile sursă și țintă" + }, + "files": { + "drag_text": "Trage aici", + "error": { + "check_type": "A apărut o eroare la verificarea tipului de fișier", + "multiple": "Încărcarea mai multor fișiere nu este permisă", + "too_large": "Fișier prea mare", + "unknown": "Citirea conținutului fișierului a eșuat" + }, + "reading": "Se citește conținutul fișierului..." + }, + "history": { + "clear": "Golește istoricul", + "clear_description": "Golirea istoricului va șterge tot istoricul traducerilor, continui?", + "delete": "Șterge istoricul traducerilor", + "empty": "Niciun istoric de traducere", + "error": { + "delete": "Ștergerea a eșuat", + "save": "Salvarea istoricului traducerilor a eșuat" + }, + "search": { + "placeholder": "Caută în istoricul traducerilor" + }, + "title": "Istoric traduceri" + }, + "info": { + "aborted": "Traducere anulată" + }, + "input": { + "placeholder": "Textul, fișierele text sau imaginile (cu suport OCR) pot fi lipite sau trase aici" + }, + "language": { + "not_pair": "Limba sursă este diferită de limba setată", + "same": "Limbile sursă și țintă sunt aceleași" + }, + "menu": { + "description": "Tradu conținutul casetei de intrare curente" + }, + "not": { + "found": "Conținutul traducerii nu a fost găsit" + }, + "output": { + "placeholder": "Traducere" + }, + "processing": "Traducere în curs...", + "settings": { + "autoCopy": "Copiază după traducere ", + "bidirectional": "Setări traducere bidirecțională", + "bidirectional_tip": "Când este activat, este acceptată doar traducerea bidirecțională între limbile sursă și țintă", + "model": "Setări model", + "model_desc": "Model folosit pentru serviciul de traducere", + "model_placeholder": "Selectează modelul de traducere", + "no_model_warning": "Niciun model de traducere selectat", + "preview": "Previzualizare Markdown", + "scroll_sync": "Setări sincronizare derulare", + "title": "Setări traducere" + }, + "success": { + "custom": { + "delete": "Șters cu succes", + "update": "Actualizare reușită" + } + }, + "target_language": "Limbă țintă", + "title": "Traducere", + "tooltip": { + "newline": "Linie nouă" + } + }, + "tray": { + "quit": "Ieșire", + "show_mini_window": "Asistent rapid", + "show_window": "Arată fereastra" + }, + "update": { + "install": "Instalează", + "later": "Mai târziu", + "message": "Noua versiune {{version}} este gata, vrei să o instalezi acum?", + "noReleaseNotes": "Nicio notă de lansare", + "saveDataError": "Salvarea datelor a eșuat, te rugăm să încerci din nou.", + "title": "Actualizare" + }, + "warning": { + "missing_provider": "Furnizorul nu există; s-a revenit la furnizorul implicit {{provider}}. Acest lucru poate cauza probleme." + }, + "words": { + "knowledgeGraph": "Grafic de cunoștințe", + "quit": "Ieșire", + "show_window": "Arată fereastra", + "visualization": "Vizualizare" + } +} diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 7e0e03a774..05ee5b8fbb 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -162,7 +162,7 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o const { message: clearMessage } = getUserMessage({ assistant, topic, type: 'clear' }) dispatch(newMessagesActions.addMessage({ topicId: topic.id, message: clearMessage })) - await saveMessageAndBlocksToDB(clearMessage, []) + await saveMessageAndBlocksToDB(topic.id, clearMessage, []) scrollToBottom() } finally { diff --git a/src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx b/src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx index 08c47311fb..ad75b75e57 100644 --- a/src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx +++ b/src/renderer/src/pages/home/Tabs/components/TopicManageMode.tsx @@ -96,7 +96,7 @@ export const TopicManagePanel: React.FC = ({ // Topics that can be selected (non-pinned, and filtered when in search mode) const selectableTopics = useMemo(() => { const baseTopics = isSearchMode ? filteredTopics : assistant.topics - return baseTopics.filter((topic) => !topic.pinned) + return (baseTopics ?? []).filter((topic) => !topic.pinned) }, [assistant.topics, filteredTopics, isSearchMode]) // Check if all selectable topics are selected diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index 3bf02a518d..b7309a0a0a 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -170,7 +170,7 @@ const AboutSettings: FC = () => { const onOpenDocs = () => { const isChinese = i18n.language.startsWith('zh') window.api.openWebsite( - isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us' + isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/docs/en-us' ) } diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index f759bcbf87..9ce67dc334 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -148,7 +148,8 @@ const GeneralSettings: FC = () => { { value: 'el-GR', label: 'Ελληνικά', flag: '🇬🇷' }, { value: 'es-ES', label: 'Español', flag: '🇪🇸' }, { value: 'fr-FR', label: 'Français', flag: '🇫🇷' }, - { value: 'pt-PT', label: 'Português', flag: '🇵🇹' } + { value: 'pt-PT', label: 'Português', flag: '🇵🇹' }, + { value: 'ro-RO', label: 'Română', flag: '🇷🇴' } ] const notificationSettings = useSelector((state: RootState) => state.settings.notification) diff --git a/src/renderer/src/pages/store/assistants/presets/assistantPresetGroupTranslations.ts b/src/renderer/src/pages/store/assistants/presets/assistantPresetGroupTranslations.ts index 953871ab30..7305585be4 100644 --- a/src/renderer/src/pages/store/assistants/presets/assistantPresetGroupTranslations.ts +++ b/src/renderer/src/pages/store/assistants/presets/assistantPresetGroupTranslations.ts @@ -11,6 +11,7 @@ export type GroupTranslations = { 'ru-RU': string 'ja-JP': string 'pt-PT': string + 'ro-RO': string } } @@ -25,7 +26,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '我的', 'ru-RU': 'Мои агенты', 'ja-JP': '私のエージェント', - 'pt-PT': 'Meus Agentes' + 'pt-PT': 'Meus Agentes', + 'ro-RO': 'Mă' }, 职业: { 'el-GR': 'Επαγγελμα', @@ -37,7 +39,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '職業', 'ru-RU': 'Карьера', 'ja-JP': 'キャリア', - 'pt-PT': 'Profissional' + 'pt-PT': 'Profissional', + 'ro-RO': 'Profesional' }, 商业: { 'el-GR': 'Εμπορικός', @@ -49,7 +52,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '商業', 'ru-RU': 'Бизнес', 'ja-JP': 'ビジネス', - 'pt-PT': 'Negócio' + 'pt-PT': 'Negócio', + 'ro-RO': 'Comercial' }, 工具: { 'el-GR': 'Εργαλεία', @@ -61,7 +65,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '工具', 'ru-RU': 'Инструменты', 'ja-JP': 'ツール', - 'pt-PT': 'Ferramentas' + 'pt-PT': 'Ferramentas', + 'ro-RO': 'Utilitare' }, 语言: { 'el-GR': 'Γλώσσα', @@ -73,7 +78,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '語言', 'ru-RU': 'Язык', 'ja-JP': '言語', - 'pt-PT': 'Idioma' + 'pt-PT': 'Idioma', + 'ro-RO': 'Limba' }, 办公: { 'el-GR': 'Γραφείο', @@ -85,7 +91,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '辦公', 'ru-RU': 'Офис', 'ja-JP': 'オフィス', - 'pt-PT': 'Escritório' + 'pt-PT': 'Escritório', + 'ro-RO': 'Oficiu' }, 通用: { 'el-GR': 'Γενικά', @@ -97,7 +104,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '通用', 'ru-RU': 'Общее', 'ja-JP': '一般', - 'pt-PT': 'Geral' + 'pt-PT': 'Geral', + 'ro-RO': 'General' }, 写作: { 'el-GR': 'Γράφημα', @@ -109,7 +117,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '寫作', 'ru-RU': 'Письмо', 'ja-JP': '書き込み', - 'pt-PT': 'Escrita' + 'pt-PT': 'Escrita', + 'ro-RO': 'Scrisoare' }, 精选: { 'el-GR': 'Επιλεγμένο', @@ -121,7 +130,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '精選', 'ru-RU': 'Избранное', 'ja-JP': '特集', - 'pt-PT': 'Destaque' + 'pt-PT': 'Destaque', + 'ro-RO': 'Recomandat' }, 编程: { 'el-GR': 'Προγραμματισμός', @@ -133,7 +143,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '編程', 'ru-RU': 'Программирование', 'ja-JP': 'プログラミング', - 'pt-PT': 'Programação' + 'pt-PT': 'Programação', + 'ro-RO': 'Programare' }, 情感: { 'el-GR': 'Αίσθημα', @@ -145,7 +156,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '情感', 'ru-RU': 'Эмоции', 'ja-JP': '感情', - 'pt-PT': 'Emoção' + 'pt-PT': 'Emoção', + 'ro-RO': 'Emoție' }, 教育: { 'el-GR': 'Εκπαίδευση', @@ -157,7 +169,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '教育', 'ru-RU': 'Образование', 'ja-JP': '教育', - 'pt-PT': 'Educação' + 'pt-PT': 'Educação', + 'ro-RO': 'Educație' }, 创意: { 'el-GR': 'Κreativiteit', @@ -169,7 +182,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '創意', 'ru-RU': 'Креатив', 'ja-JP': 'クリエイティブ', - 'pt-PT': 'Criativo' + 'pt-PT': 'Criativo', + 'ro-RO': 'Creativ' }, 学术: { 'el-GR': 'Ακαδημικός', @@ -181,7 +195,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '學術', 'ru-RU': 'Академический', 'ja-JP': 'アカデミック', - 'pt-PT': 'Académico' + 'pt-PT': 'Académico', + 'ro-RO': 'Academic' }, 设计: { 'el-GR': 'Δημιουργικό', @@ -193,7 +208,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '設計', 'ru-RU': 'Дизайн', 'ja-JP': 'デザイン', - 'pt-PT': 'Design' + 'pt-PT': 'Design', + 'ro-RO': 'Design' }, 艺术: { 'el-GR': 'Τέχνη', @@ -205,7 +221,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '藝術', 'ru-RU': 'Искусство', 'ja-JP': 'アート', - 'pt-PT': 'Arte' + 'pt-PT': 'Arte', + 'ro-RO': 'Art' }, 娱乐: { 'el-GR': 'Αναψυχή', @@ -217,7 +234,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '娛樂', 'ru-RU': 'Развлечения', 'ja-JP': 'エンターテイメント', - 'pt-PT': 'Entretenimento' + 'pt-PT': 'Entretenimento', + 'ro-RO': 'Entertainment' }, 生活: { 'el-GR': 'Ζωή', @@ -229,7 +247,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '生活', 'ru-RU': 'Жизнь', 'ja-JP': '生活', - 'pt-PT': 'Vida' + 'pt-PT': 'Vida', + 'ro-RO': 'Life' }, 医疗: { 'el-GR': 'Υγεία', @@ -241,7 +260,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '醫療', 'ru-RU': 'Медицина', 'ja-JP': '医療', - 'pt-PT': 'Saúde' + 'pt-PT': 'Saúde', + 'ro-RO': 'Medical' }, 游戏: { 'el-GR': 'Παιχνίδια', @@ -253,7 +273,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '遊戲', 'ru-RU': 'Игры', 'ja-JP': 'ゲーム', - 'pt-PT': 'Jogos' + 'pt-PT': 'Jogos', + 'ro-RO': 'Games' }, 翻译: { 'el-GR': 'Γραφήματα', @@ -265,7 +286,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '翻譯', 'ru-RU': 'Перевод', 'ja-JP': '翻訳', - 'pt-PT': 'Tradução' + 'pt-PT': 'Tradução', + 'ro-RO': 'Translation' }, 音乐: { 'el-GR': 'Μουσική', @@ -277,7 +299,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '音樂', 'ru-RU': 'Музыка', 'ja-JP': '音楽', - 'pt-PT': 'Música' + 'pt-PT': 'Música', + 'ro-RO': 'Music' }, 点评: { 'el-GR': 'Αξιολόγηση', @@ -289,7 +312,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '點評', 'ru-RU': 'Обзор', 'ja-JP': 'レビュー', - 'pt-PT': 'Revisão' + 'pt-PT': 'Revisão', + 'ro-RO': 'Review' }, 文案: { 'el-GR': 'Γραφήματα', @@ -301,7 +325,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '文案', 'ru-RU': 'Копирайтинг', 'ja-JP': 'コピーライティング', - 'pt-PT': 'Escrita' + 'pt-PT': 'Escrita', + 'ro-RO': 'Copywriting' }, 百科: { 'el-GR': 'Εγκυκλοπαίδεια', @@ -313,7 +338,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '百科', 'ru-RU': 'Энциклопедия', 'ja-JP': '百科事典', - 'pt-PT': 'Enciclopédia' + 'pt-PT': 'Enciclopédia', + 'ro-RO': 'Encyclopedia' }, 健康: { 'el-GR': 'Υγεία', @@ -325,7 +351,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '健康', 'ru-RU': 'Здоровье', 'ja-JP': '健康', - 'pt-PT': 'Saúde' + 'pt-PT': 'Saúde', + 'ro-RO': 'Health' }, 营销: { 'el-GR': 'Μάρκετινγκ', @@ -337,7 +364,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '營銷', 'ru-RU': 'Маркетинг', 'ja-JP': 'マーケティング', - 'pt-PT': 'Marketing' + 'pt-PT': 'Marketing', + 'ro-RO': 'Marketing' }, 科学: { 'el-GR': 'Επιστήμη', @@ -349,7 +377,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '科學', 'ru-RU': 'Наука', 'ja-JP': '科学', - 'pt-PT': 'Ciência' + 'pt-PT': 'Ciência', + 'ro-RO': 'Science' }, 分析: { 'el-GR': 'Ανάλυση', @@ -361,7 +390,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '分析', 'ru-RU': 'Анализ', 'ja-JP': '分析', - 'pt-PT': 'Análise' + 'pt-PT': 'Análise', + 'ro-RO': 'Analysis' }, 法律: { 'el-GR': 'Νόμος', @@ -373,7 +403,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '法律', 'ru-RU': 'Право', 'ja-JP': '法律', - 'pt-PT': 'Legal' + 'pt-PT': 'Legal', + 'ro-RO': 'Legal' }, 咨询: { 'el-GR': 'Συμβουλή', @@ -385,7 +416,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '諮詢', 'ru-RU': 'Консалтинг', 'ja-JP': 'コンサルティング', - 'pt-PT': 'Consultoria' + 'pt-PT': 'Consultoria', + 'ro-RO': 'Consulting' }, 金融: { 'el-GR': 'Φορολογία', @@ -397,7 +429,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '金融', 'ru-RU': 'Финансы', 'ja-JP': '金融', - 'pt-PT': 'Finanças' + 'pt-PT': 'Finanças', + 'ro-RO': 'Finance' }, 旅游: { 'el-GR': 'Τουρισμός', @@ -409,7 +442,8 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '旅遊', 'ru-RU': 'Путешествия', 'ja-JP': '旅行', - 'pt-PT': 'Viagens' + 'pt-PT': 'Viagens', + 'ro-RO': 'Travel' }, 管理: { 'el-GR': 'Διοίκηση', @@ -421,6 +455,7 @@ export const groupTranslations: GroupTranslations = { 'zh-TW': '管理', 'ru-RU': 'Управление', 'ja-JP': '管理', - 'pt-PT': 'Gestão' + 'pt-PT': 'Gestão', + 'ro-RO': 'Management' } } diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 6f4ec188da..97f2a6f179 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -116,7 +116,6 @@ export function getDefaultTranslateAssistant( // disable reasoning if it could be disabled, otherwise no configuration const reasoningEffort = supportedOptions?.includes('none') ? 'none' : 'default' const settings = { - temperature: 0.7, reasoning_effort: reasoningEffort, ..._settings } satisfies Partial diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 45d7fd760a..aaa2ffc2c4 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -19,6 +19,7 @@ import { AiSdkToChunkAdapter } from '@renderer/aiCore/chunk/AiSdkToChunkAdapter' import { AgentApiClient } from '@renderer/api/agent' import db from '@renderer/databases' import { fetchMessagesSummary, transformMessagesAndFetch } from '@renderer/services/ApiService' +import { dbService } from '@renderer/services/db' import { DbService } from '@renderer/services/db/DbService' import FileManager from '@renderer/services/FileManager' import { BlockManager } from '@renderer/services/messageStreaming/BlockManager' @@ -57,18 +58,18 @@ import { mutate } from 'swr' import type { AppDispatch, RootState } from '../index' import { removeManyBlocks, updateOneBlock, upsertManyBlocks, upsertOneBlock } from '../messageBlock' import { newMessagesActions, selectMessagesForTopic } from '../newMessage' -import { - bulkAddBlocksV2, - clearMessagesFromDBV2, - deleteMessageFromDBV2, - deleteMessagesFromDBV2, - loadTopicMessagesThunkV2, - saveMessageAndBlocksToDBV2, - updateBlocksV2, - updateFileCountV2, - updateMessageV2, - updateSingleBlockV2 -} from './messageThunk.v2' +// import { +// bulkAddBlocksV2, +// clearMessagesFromDBV2, +// deleteMessageFromDBV2, +// deleteMessagesFromDBV2, +// loadTopicMessagesThunkV2, +// saveMessageAndBlocksToDBV2, +// updateBlocksV2, +// updateFileCountV2, +// updateMessageV2, +// updateSingleBlockV2 +// } from './messageThunk.v2' const logger = loggerService.withContext('MessageThunk') @@ -363,9 +364,9 @@ const createAgentMessageStream = async ( return createSSEReadableStream(response.body, signal) } // TODO: 后续可以将db操作移到Listener Middleware中 -export const saveMessageAndBlocksToDB = async (message: Message, blocks: MessageBlock[], messageIndex: number = -1) => { - return saveMessageAndBlocksToDBV2(message.topicId, message, blocks, messageIndex) -} +// export const saveMessageAndBlocksToDB = async (message: Message, blocks: MessageBlock[], messageIndex: number = -1) => { +// return saveMessageAndBlocksToDBV2(message.topicId, message, blocks, messageIndex) +// } const updateExistingMessageAndBlocksInDB = async ( updatedMessage: Partial & Pick, @@ -374,7 +375,7 @@ const updateExistingMessageAndBlocksInDB = async ( try { // Always update blocks if provided if (updatedBlocks.length > 0) { - await updateBlocksV2(updatedBlocks) + await updateBlocks(updatedBlocks) } // Check if there are message properties to update beyond id and topicId @@ -386,7 +387,7 @@ const updateExistingMessageAndBlocksInDB = async ( return acc }, {}) - await updateMessageV2(updatedMessage.topicId, updatedMessage.id, messageUpdatesPayload) + await updateMessage(updatedMessage.topicId, updatedMessage.id, messageUpdatesPayload) store.dispatch(updateTopicUpdatedAt({ topicId: updatedMessage.topicId })) } @@ -432,7 +433,7 @@ const getBlockThrottler = (id: string) => { }) blockUpdateRafs.set(id, rafId) - await updateSingleBlockV2(id, blockUpdate) + await updateSingleBlock(id, blockUpdate) }, 150) blockUpdateThrottlers.set(id, throttler) @@ -893,7 +894,7 @@ export const sendMessage = userMessage.agentSessionId = activeAgentSession.agentSessionId } - await saveMessageAndBlocksToDB(userMessage, userMessageBlocks) + await saveMessageAndBlocksToDB(topicId, userMessage, userMessageBlocks) dispatch(newMessagesActions.addMessage({ topicId, message: userMessage })) if (userMessageBlocks.length > 0) { dispatch(upsertManyBlocks(userMessageBlocks)) @@ -911,7 +912,7 @@ export const sendMessage = if (activeAgentSession.agentSessionId && !assistantMessage.agentSessionId) { assistantMessage.agentSessionId = activeAgentSession.agentSessionId } - await saveMessageAndBlocksToDB(assistantMessage, []) + await saveMessageAndBlocksToDB(topicId, assistantMessage, []) dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage })) queue.add(async () => { @@ -934,7 +935,7 @@ export const sendMessage = model: assistant.model, traceId: userMessage.traceId }) - await saveMessageAndBlocksToDB(assistantMessage, []) + await saveMessageAndBlocksToDB(topicId, assistantMessage, []) dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage })) queue.add(async () => { @@ -1000,11 +1001,11 @@ export const loadAgentSessionMessagesThunk = * Loads messages and their blocks for a specific topic from the database * and updates the Redux store. */ -export const loadTopicMessagesThunk = - (topicId: string, forceReload: boolean = false) => - async (dispatch: AppDispatch, getState: () => RootState) => { - return loadTopicMessagesThunkV2(topicId, forceReload)(dispatch, getState) - } +// export const loadTopicMessagesThunk = +// (topicId: string, forceReload: boolean = false) => +// async (dispatch: AppDispatch, getState: () => RootState) => { +// return loadTopicMessagesThunkV2(topicId, forceReload)(dispatch, getState) +// } /** * Thunk to delete a single message and its associated blocks. @@ -1023,7 +1024,7 @@ export const deleteSingleMessageThunk = try { dispatch(newMessagesActions.removeMessage({ topicId, messageId })) cleanupMultipleBlocks(dispatch, blockIdsToDelete) - await deleteMessageFromDBV2(topicId, messageId) + await deleteMessageFromDB(topicId, messageId) } catch (error) { logger.error(`[deleteSingleMessage] Failed to delete message ${messageId}:`, error as Error) } @@ -1062,7 +1063,7 @@ export const deleteMessageGroupThunk = try { dispatch(newMessagesActions.removeMessagesByAskId({ topicId, askId })) cleanupMultipleBlocks(dispatch, blockIdsToDelete) - await deleteMessagesFromDBV2(topicId, messageIdsToDelete) + await deleteMessagesFromDB(topicId, messageIdsToDelete) } catch (error) { logger.error(`[deleteMessageGroup] Failed to delete messages with askId ${askId}:`, error as Error) } @@ -1087,7 +1088,7 @@ export const clearTopicMessagesThunk = dispatch(newMessagesActions.clearTopicMessages(topicId)) cleanupMultipleBlocks(dispatch, blockIdsToDelete) - await clearMessagesFromDBV2(topicId) + await clearMessagesFromDB(topicId) } catch (error) { logger.error(`[clearTopicMessagesThunk] Failed to clear messages for topic ${topicId}:`, error as Error) } @@ -1408,7 +1409,7 @@ export const updateTranslationBlockThunk = // 更新Redux状态 dispatch(updateOneBlock({ id: blockId, changes })) - await updateSingleBlockV2(blockId, changes) + await updateSingleBlock(blockId, changes) // Logger.log(`[updateTranslationBlockThunk] Successfully updated translation block ${blockId}.`) } catch (error) { logger.error(`[updateTranslationBlockThunk] Failed to update translation block ${blockId}:`, error as Error) @@ -1479,7 +1480,7 @@ export const appendAssistantResponseThunk = const insertAtIndex = existingMessageIndex !== -1 ? existingMessageIndex + 1 : currentTopicMessageIds.length // 4. Update Database (Save the stub to the topic's message list) - await saveMessageAndBlocksToDB(newAssistantMessageStub, [], insertAtIndex) + await saveMessageAndBlocksToDB(topicId, newAssistantMessageStub, [], insertAtIndex) dispatch( newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantMessageStub, index: insertAtIndex }) @@ -1631,12 +1632,12 @@ export const cloneMessagesToNewTopicThunk = // Add the NEW blocks if (clonedBlocks.length > 0) { - await bulkAddBlocksV2(clonedBlocks) + await bulkAddBlocks(clonedBlocks) } // Update file counts const uniqueFiles = [...new Map(filesToUpdateCount.map((f) => [f.id, f])).values()] for (const file of uniqueFiles) { - await updateFileCountV2(file.id, 1, false) + await updateFileCount(file.id, 1, false) } }) @@ -1690,11 +1691,11 @@ export const updateMessageAndBlocksThunk = } // Update message properties if provided if (messageUpdates && Object.keys(messageUpdates).length > 0 && messageId) { - await updateMessageV2(topicId, messageId, messageUpdates) + await updateMessage(topicId, messageId, messageUpdates) } // Update blocks if provided if (blockUpdatesList.length > 0) { - await updateBlocksV2(blockUpdatesList) + await updateBlocks(blockUpdatesList) } dispatch(updateTopicUpdatedAt({ topicId })) @@ -1748,3 +1749,197 @@ export const removeBlocksThunk = throw error } } + +//以下内容从原 messageThunk.v2.ts 迁移过来,原文件已经删除 +//原因:v2.ts并不是v2数据重构的一部分,而相关命名对v2重构造成重大误解,故两文件合并,以消除误解 + +/** + * Load messages for a topic using unified DbService + */ +export const loadTopicMessagesThunk = + (topicId: string, forceReload: boolean = false) => + async (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState() + + dispatch(newMessagesActions.setCurrentTopicId(topicId)) + + // Skip if already cached and not forcing reload + if (!forceReload && state.messages.messageIdsByTopic[topicId]) { + return + } + + try { + dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true })) + + // Unified call - no need to check isAgentSessionTopicId + const { messages, blocks } = await dbService.fetchMessages(topicId) + + logger.silly('Loaded messages via DbService', { + topicId, + messageCount: messages.length, + blockCount: blocks.length + }) + + // Update Redux state with fetched data + if (blocks.length > 0) { + dispatch(upsertManyBlocks(blocks)) + } + dispatch(newMessagesActions.messagesReceived({ topicId, messages })) + } catch (error) { + logger.error(`Failed to load messages for topic ${topicId}:`, error as Error) + // Could dispatch an error action here if needed + } finally { + dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false })) + } + } + +/** + * Get raw topic data using unified DbService + * Returns topic with messages array + */ +export const getRawTopic = async (topicId: string): Promise<{ id: string; messages: Message[] } | undefined> => { + try { + const rawTopic = await dbService.getRawTopic(topicId) + logger.silly('Retrieved raw topic via DbService', { topicId, found: !!rawTopic }) + return rawTopic + } catch (error) { + logger.error('Failed to get raw topic:', { topicId, error }) + return undefined + } +} + +/** + * Update file reference count + * Only applies to Dexie data source, no-op for agent sessions + */ +export const updateFileCount = async (fileId: string, delta: number, deleteIfZero: boolean = false): Promise => { + try { + // Pass all parameters to dbService, including deleteIfZero + await dbService.updateFileCount(fileId, delta, deleteIfZero) + logger.silly('Updated file count', { fileId, delta, deleteIfZero }) + } catch (error) { + logger.error('Failed to update file count:', { fileId, delta, error }) + throw error + } +} + +/** + * Delete a single message from database + */ +export const deleteMessageFromDB = async (topicId: string, messageId: string): Promise => { + try { + await dbService.deleteMessage(topicId, messageId) + logger.silly('Deleted message via DbService', { topicId, messageId }) + } catch (error) { + logger.error('Failed to delete message:', { topicId, messageId, error }) + throw error + } +} + +/** + * Delete multiple messages from database + */ +export const deleteMessagesFromDB = async (topicId: string, messageIds: string[]): Promise => { + try { + await dbService.deleteMessages(topicId, messageIds) + logger.silly('Deleted messages via DbService', { topicId, count: messageIds.length }) + } catch (error) { + logger.error('Failed to delete messages:', { topicId, messageIds, error }) + throw error + } +} + +/** + * Clear all messages from a topic + */ +export const clearMessagesFromDB = async (topicId: string): Promise => { + try { + await dbService.clearMessages(topicId) + logger.silly('Cleared all messages via DbService', { topicId }) + } catch (error) { + logger.error('Failed to clear messages:', { topicId, error }) + throw error + } +} + +/** + * Save a message and its blocks to database + * Uses unified interface, no need for isAgentSessionTopicId check + */ +export const saveMessageAndBlocksToDB = async ( + topicId: string, + message: Message, + blocks: MessageBlock[], + messageIndex: number = -1 +): Promise => { + try { + const blockIds = blocks.map((block) => block.id) + const shouldSyncBlocks = + blockIds.length > 0 && (!message.blocks || blockIds.some((id, index) => message.blocks?.[index] !== id)) + + const messageWithBlocks = shouldSyncBlocks ? { ...message, blocks: blockIds } : message + // Direct call without conditional logic, now with messageIndex + await dbService.appendMessage(topicId, messageWithBlocks, blocks, messageIndex) + logger.silly('Saved message and blocks via DbService', { + topicId, + messageId: message.id, + blockCount: blocks.length, + messageIndex + }) + } catch (error) { + logger.error('Failed to save message and blocks:', { topicId, messageId: message.id, error }) + throw error + } +} + +/** + * Update a message in the database + */ +export const updateMessage = async (topicId: string, messageId: string, updates: Partial): Promise => { + try { + await dbService.updateMessage(topicId, messageId, updates) + logger.silly('Updated message via DbService', { topicId, messageId }) + } catch (error) { + logger.error('Failed to update message:', { topicId, messageId, error }) + throw error + } +} + +/** + * Update a single message block + */ +export const updateSingleBlock = async (blockId: string, updates: Partial): Promise => { + try { + await dbService.updateSingleBlock(blockId, updates) + logger.silly('Updated single block via DbService', { blockId }) + } catch (error) { + logger.error('Failed to update single block:', { blockId, error }) + throw error + } +} + +/** + * Bulk add message blocks (for new blocks) + */ +export const bulkAddBlocks = async (blocks: MessageBlock[]): Promise => { + try { + await dbService.bulkAddBlocks(blocks) + logger.silly('Bulk added blocks via DbService', { count: blocks.length }) + } catch (error) { + logger.error('Failed to bulk add blocks:', { count: blocks.length, error }) + throw error + } +} + +/** + * Update multiple message blocks (upsert operation) + */ +export const updateBlocks = async (blocks: MessageBlock[]): Promise => { + try { + await dbService.updateBlocks(blocks) + logger.silly('Updated blocks via DbService', { count: blocks.length }) + } catch (error) { + logger.error('Failed to update blocks:', { count: blocks.length, error }) + throw error + } +} diff --git a/src/renderer/src/store/thunk/messageThunk.v2.ts b/src/renderer/src/store/thunk/messageThunk.v2.ts deleted file mode 100644 index 587a9baf68..0000000000 --- a/src/renderer/src/store/thunk/messageThunk.v2.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * @deprecated Scheduled for removal in v2.0.0 - * -------------------------------------------------------------------------- - * ⚠️ NOTICE: V2 DATA&UI REFACTORING (by 0xfullex) - * -------------------------------------------------------------------------- - * STOP: Feature PRs affecting this file are currently BLOCKED. - * Only critical bug fixes are accepted during this migration phase. - * - * This file is being refactored to v2 standards. - * Any non-critical changes will conflict with the ongoing work. - * - * 🔗 Context & Status: - * - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954 - * - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162 - * -------------------------------------------------------------------------- - */ -/** - * V2 implementations of message thunk functions using the unified DbService - * These implementations will be gradually rolled out using feature flags - */ - -import { loggerService } from '@logger' -import { dbService } from '@renderer/services/db' -import type { Message, MessageBlock } from '@renderer/types/newMessage' - -import type { AppDispatch, RootState } from '../index' -import { upsertManyBlocks } from '../messageBlock' -import { newMessagesActions } from '../newMessage' - -const logger = loggerService.withContext('MessageThunkV2') - -// ================================================================= -// Phase 2.1 - Batch 1: Read-only operations (lowest risk) -// ================================================================= - -/** - * Load messages for a topic using unified DbService - * This is the V2 implementation that will replace the original - */ -export const loadTopicMessagesThunkV2 = - (topicId: string, forceReload: boolean = false) => - async (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState() - - dispatch(newMessagesActions.setCurrentTopicId(topicId)) - - // Skip if already cached and not forcing reload - if (!forceReload && state.messages.messageIdsByTopic[topicId]) { - return - } - - try { - dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true })) - - // Unified call - no need to check isAgentSessionTopicId - const { messages, blocks } = await dbService.fetchMessages(topicId) - - logger.silly('Loaded messages via DbService', { - topicId, - messageCount: messages.length, - blockCount: blocks.length - }) - - // Update Redux state with fetched data - if (blocks.length > 0) { - dispatch(upsertManyBlocks(blocks)) - } - dispatch(newMessagesActions.messagesReceived({ topicId, messages })) - } catch (error) { - logger.error(`Failed to load messages for topic ${topicId}:`, error as Error) - // Could dispatch an error action here if needed - } finally { - dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false })) - } - } - -/** - * Get raw topic data using unified DbService - * Returns topic with messages array - */ -export const getRawTopicV2 = async (topicId: string): Promise<{ id: string; messages: Message[] } | undefined> => { - try { - const rawTopic = await dbService.getRawTopic(topicId) - logger.silly('Retrieved raw topic via DbService', { topicId, found: !!rawTopic }) - return rawTopic - } catch (error) { - logger.error('Failed to get raw topic:', { topicId, error }) - return undefined - } -} - -// ================================================================= -// Phase 2.2 - Batch 2: Helper functions -// ================================================================= - -/** - * Update file reference count - * Only applies to Dexie data source, no-op for agent sessions - */ -export const updateFileCountV2 = async ( - fileId: string, - delta: number, - deleteIfZero: boolean = false -): Promise => { - try { - // Pass all parameters to dbService, including deleteIfZero - await dbService.updateFileCount(fileId, delta, deleteIfZero) - logger.silly('Updated file count', { fileId, delta, deleteIfZero }) - } catch (error) { - logger.error('Failed to update file count:', { fileId, delta, error }) - throw error - } -} - -// ================================================================= -// Phase 2.3 - Batch 3: Delete operations -// ================================================================= - -/** - * Delete a single message from database - */ -export const deleteMessageFromDBV2 = async (topicId: string, messageId: string): Promise => { - try { - await dbService.deleteMessage(topicId, messageId) - logger.silly('Deleted message via DbService', { topicId, messageId }) - } catch (error) { - logger.error('Failed to delete message:', { topicId, messageId, error }) - throw error - } -} - -/** - * Delete multiple messages from database - */ -export const deleteMessagesFromDBV2 = async (topicId: string, messageIds: string[]): Promise => { - try { - await dbService.deleteMessages(topicId, messageIds) - logger.silly('Deleted messages via DbService', { topicId, count: messageIds.length }) - } catch (error) { - logger.error('Failed to delete messages:', { topicId, messageIds, error }) - throw error - } -} - -/** - * Clear all messages from a topic - */ -export const clearMessagesFromDBV2 = async (topicId: string): Promise => { - try { - await dbService.clearMessages(topicId) - logger.silly('Cleared all messages via DbService', { topicId }) - } catch (error) { - logger.error('Failed to clear messages:', { topicId, error }) - throw error - } -} - -// ================================================================= -// Phase 2.4 - Batch 4: Complex write operations -// ================================================================= - -/** - * Save a message and its blocks to database - * Uses unified interface, no need for isAgentSessionTopicId check - */ -export const saveMessageAndBlocksToDBV2 = async ( - topicId: string, - message: Message, - blocks: MessageBlock[], - messageIndex: number = -1 -): Promise => { - try { - const blockIds = blocks.map((block) => block.id) - const shouldSyncBlocks = - blockIds.length > 0 && (!message.blocks || blockIds.some((id, index) => message.blocks?.[index] !== id)) - - const messageWithBlocks = shouldSyncBlocks ? { ...message, blocks: blockIds } : message - // Direct call without conditional logic, now with messageIndex - await dbService.appendMessage(topicId, messageWithBlocks, blocks, messageIndex) - logger.silly('Saved message and blocks via DbService', { - topicId, - messageId: message.id, - blockCount: blocks.length, - messageIndex - }) - } catch (error) { - logger.error('Failed to save message and blocks:', { topicId, messageId: message.id, error }) - throw error - } -} - -// Note: sendMessageV2 would be implemented here but it's more complex -// and would require more of the supporting code from messageThunk.ts - -// ================================================================= -// Phase 2.5 - Batch 5: Update operations -// ================================================================= - -/** - * Update a message in the database - */ -export const updateMessageV2 = async (topicId: string, messageId: string, updates: Partial): Promise => { - try { - await dbService.updateMessage(topicId, messageId, updates) - logger.silly('Updated message via DbService', { topicId, messageId }) - } catch (error) { - logger.error('Failed to update message:', { topicId, messageId, error }) - throw error - } -} - -/** - * Update a single message block - */ -export const updateSingleBlockV2 = async (blockId: string, updates: Partial): Promise => { - try { - await dbService.updateSingleBlock(blockId, updates) - logger.silly('Updated single block via DbService', { blockId }) - } catch (error) { - logger.error('Failed to update single block:', { blockId, error }) - throw error - } -} - -/** - * Bulk add message blocks (for new blocks) - */ -export const bulkAddBlocksV2 = async (blocks: MessageBlock[]): Promise => { - try { - await dbService.bulkAddBlocks(blocks) - logger.silly('Bulk added blocks via DbService', { count: blocks.length }) - } catch (error) { - logger.error('Failed to bulk add blocks:', { count: blocks.length, error }) - throw error - } -} - -/** - * Update multiple message blocks (upsert operation) - */ -export const updateBlocksV2 = async (blocks: MessageBlock[]): Promise => { - try { - await dbService.updateBlocks(blocks) - logger.silly('Updated blocks via DbService', { count: blocks.length }) - } catch (error) { - logger.error('Failed to update blocks:', { count: blocks.length, error }) - throw error - } -} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 70b8999901..e2fd7a40ba 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -474,6 +474,7 @@ export type LanguageVarious = | 'fr-FR' | 'ja-JP' | 'pt-PT' + | 'ro-RO' | 'ru-RU' export type CodeStyleVarious = 'auto' | string diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index 4e01649369..d724bdb35e 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -83,9 +83,7 @@ const detectLanguageByLLM = async (inputText: string): Promise void = (chunk: Chunk) => { @@ -257,6 +255,7 @@ export const getTranslateOptions = async () => { })) return [...builtinLanguages, ...transformedCustomLangs] } catch (e) { + logger.error('[getTranslateOptions] Failed to get custom languages. Fallback to builtinLanguages', e as Error) return builtinLanguages } } diff --git a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx index 5e83071add..b5e0fea689 100644 --- a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx @@ -11,7 +11,6 @@ import MessageContent from '@renderer/pages/home/Messages/MessageContent' import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import type { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types' import type { ActionItem } from '@renderer/types/selectionTypes' -import { runAsyncFunction } from '@renderer/utils' import { abortCompletion } from '@renderer/utils/abortController' import { detectLanguage } from '@renderer/utils/translate' import { Tooltip } from 'antd' @@ -32,70 +31,102 @@ const logger = loggerService.withContext('ActionTranslate') const ActionTranslate: FC = ({ action, scrollToBottom }) => { const { t } = useTranslation() - const { translateModelPrompt, language } = useSettings() + const { language } = useSettings() + const { getLanguageByLangcode, isLoaded: isLanguagesLoaded } = useTranslate() - const [targetLanguage, setTargetLanguage] = useState(LanguagesEnum.enUS) - const [alterLanguage, setAlterLanguage] = useState(LanguagesEnum.zhCN) + const [targetLanguage, setTargetLanguage] = useState(() => { + const lang = getLanguageByLangcode(language) + if (lang !== UNKNOWN) { + return lang + } else { + logger.warn('[initialize targetLanguage] Unexpected UNKNOWN. Fallback to zh-CN') + return LanguagesEnum.zhCN + } + }) + + const [alterLanguage, setAlterLanguage] = useState(LanguagesEnum.enUS) const [error, setError] = useState('') const [showOriginal, setShowOriginal] = useState(false) const [isContented, setIsContented] = useState(false) const [isLoading, setIsLoading] = useState(true) const [contentToCopy, setContentToCopy] = useState('') - const { getLanguageByLangcode } = useTranslate() // Use useRef for values that shouldn't trigger re-renders const initialized = useRef(false) const assistantRef = useRef(null) const topicRef = useRef(null) const askId = useRef('') + const targetLangRef = useRef(targetLanguage) - useEffect(() => { - runAsyncFunction(async () => { - const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) + // It's called only in initialization. + // It will change target/alter language, so fetchResult will be triggered. Be careful! + const updateLanguagePair = useCallback(async () => { + // Only called is when languages loaded. + // It ensure we could get right language from getLanguageByLangcode. + if (!isLanguagesLoaded) { + logger.silly('[updateLanguagePair] Languages are not loaded. Skip.') + return + } - let targetLang: TranslateLanguage - let alterLang: TranslateLanguage - - if (!biDirectionLangPair || !biDirectionLangPair.value[0]) { - const lang = getLanguageByLangcode(language) - if (lang !== UNKNOWN) { - targetLang = lang - } else { - logger.warn('Fallback to zh-CN') - targetLang = LanguagesEnum.zhCN - } - } else { - targetLang = getLanguageByLangcode(biDirectionLangPair.value[0]) - } - - if (!biDirectionLangPair || !biDirectionLangPair.value[1]) { - alterLang = LanguagesEnum.enUS - } else { - alterLang = getLanguageByLangcode(biDirectionLangPair.value[1]) - } + const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) + if (biDirectionLangPair && biDirectionLangPair.value[0]) { + const targetLang = getLanguageByLangcode(biDirectionLangPair.value[0]) setTargetLanguage(targetLang) - setAlterLanguage(alterLang) - }) - }, [getLanguageByLangcode, language]) + targetLangRef.current = targetLang + } - // Initialize values only once when action changes - useEffect(() => { - if (initialized.current || !action.selectedText) return - initialized.current = true + if (biDirectionLangPair && biDirectionLangPair.value[1]) { + const alterLang = getLanguageByLangcode(biDirectionLangPair.value[1]) + setAlterLanguage(alterLang) + } + }, [getLanguageByLangcode, isLanguagesLoaded]) + + // Initialize values only once + const initialize = useCallback(async () => { + if (initialized.current) { + logger.silly('[initialize] Already initialized.') + return + } + + // Only try to initialize when languages loaded, so updateLanguagePair would not fail. + if (!isLanguagesLoaded) { + logger.silly('[initialize] Languages not loaded. Skip initialization.') + return + } + + // Edge case + if (action.selectedText === undefined) { + logger.error('[initialize] No selected text.') + return + } + logger.silly('[initialize] Start initialization.') + + // Initialize language pair. + // It will update targetLangRef, so we could get latest target language in the following code + await updateLanguagePair() // Initialize assistant - const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText) + const currentAssistant = getDefaultTranslateAssistant(targetLangRef.current, action.selectedText) assistantRef.current = currentAssistant // Initialize topic topicRef.current = getDefaultTopic(currentAssistant.id) - }, [action, targetLanguage, translateModelPrompt]) + initialized.current = true + }, [action.selectedText, isLanguagesLoaded, updateLanguagePair]) + + // Try to initialize when: + // 1. action.selectedText change (generally will not) + // 2. isLanguagesLoaded change (only initialize when languages loaded) + // 3. updateLanguagePair change (depend on translateLanguages and isLanguagesLoaded) + useEffect(() => { + initialize() + }, [initialize]) const fetchResult = useCallback(async () => { - if (!assistantRef.current || !topicRef.current || !action.selectedText) return + if (!assistantRef.current || !topicRef.current || !action.selectedText || !initialized.current) return const setAskId = (id: string) => { askId.current = id @@ -141,6 +172,7 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText) assistantRef.current = assistant + logger.debug('process once') processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError) }, [action, targetLanguage, alterLanguage, scrollToBottom]) @@ -157,7 +189,11 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { }, [allMessages]) const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => { + if (!initialized.current) { + return + } setTargetLanguage(targetLanguage) + targetLangRef.current = targetLanguage setAlterLanguage(alterLanguage) db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] })