mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/hitl-frontend
This commit is contained in:
commit
aa9d0bd655
|
|
@ -0,0 +1,205 @@
|
|||
# Test Generation Checklist
|
||||
|
||||
Use this checklist when generating or reviewing tests for Dify frontend components.
|
||||
|
||||
## Pre-Generation
|
||||
|
||||
- [ ] Read the component source code completely
|
||||
- [ ] Identify component type (component, hook, utility, page)
|
||||
- [ ] Run `pnpm analyze-component <path>` if available
|
||||
- [ ] Note complexity score and features detected
|
||||
- [ ] Check for existing tests in the same directory
|
||||
- [ ] **Identify ALL files in the directory** that need testing (not just index)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### ⚠️ Incremental Workflow (CRITICAL for Multi-File)
|
||||
|
||||
- [ ] **NEVER generate all tests at once** - process one file at a time
|
||||
- [ ] Order files by complexity: utilities → hooks → simple → complex → integration
|
||||
- [ ] Create a todo list to track progress before starting
|
||||
- [ ] For EACH file: write → run test → verify pass → then next
|
||||
- [ ] **DO NOT proceed** to next file until current one passes
|
||||
|
||||
### Path-Level Coverage
|
||||
|
||||
- [ ] **Test ALL files** in the assigned directory/path
|
||||
- [ ] List all components, hooks, utilities that need coverage
|
||||
- [ ] Decide: single spec file (integration) or multiple spec files (unit)
|
||||
|
||||
### Complexity Assessment
|
||||
|
||||
- [ ] Run `pnpm analyze-component <path>` for complexity score
|
||||
- [ ] **Complexity > 50**: Consider refactoring before testing
|
||||
- [ ] **500+ lines**: Consider splitting before testing
|
||||
- [ ] **30-50 complexity**: Use multiple describe blocks, organized structure
|
||||
|
||||
### Integration vs Mocking
|
||||
|
||||
- [ ] **DO NOT mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
||||
- [ ] Import real project components instead of mocking
|
||||
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
|
||||
- [ ] Prefer integration testing when using single spec file
|
||||
|
||||
## Required Test Sections
|
||||
|
||||
### All Components MUST Have
|
||||
|
||||
- [ ] **Rendering tests** - Component renders without crashing
|
||||
- [ ] **Props tests** - Required props, optional props, default values
|
||||
- [ ] **Edge cases** - null, undefined, empty values, boundaries
|
||||
|
||||
### Conditional Sections (Add When Feature Present)
|
||||
|
||||
| Feature | Add Tests For |
|
||||
|---------|---------------|
|
||||
| `useState` | Initial state, transitions, cleanup |
|
||||
| `useEffect` | Execution, dependencies, cleanup |
|
||||
| Event handlers | onClick, onChange, onSubmit, keyboard |
|
||||
| API calls | Loading, success, error states |
|
||||
| Routing | Navigation, params, query strings |
|
||||
| `useCallback`/`useMemo` | Referential equality |
|
||||
| Context | Provider values, consumer behavior |
|
||||
| Forms | Validation, submission, error display |
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
### Structure
|
||||
|
||||
- [ ] Uses `describe` blocks to group related tests
|
||||
- [ ] Test names follow `should <behavior> when <condition>` pattern
|
||||
- [ ] AAA pattern (Arrange-Act-Assert) is clear
|
||||
- [ ] Comments explain complex test scenarios
|
||||
|
||||
### Mocks
|
||||
|
||||
- [ ] **DO NOT mock base components** (`@/app/components/base/*`)
|
||||
- [ ] `jest.clearAllMocks()` in `beforeEach` (not `afterEach`)
|
||||
- [ ] Shared mock state reset in `beforeEach`
|
||||
- [ ] i18n mock returns keys (not empty strings)
|
||||
- [ ] Router mocks match actual Next.js API
|
||||
- [ ] Mocks reflect actual component conditional behavior
|
||||
- [ ] Only mock: API services, complex context providers, third-party libs
|
||||
|
||||
### Queries
|
||||
|
||||
- [ ] Prefer semantic queries (`getByRole`, `getByLabelText`)
|
||||
- [ ] Use `queryBy*` for absence assertions
|
||||
- [ ] Use `findBy*` for async elements
|
||||
- [ ] `getByTestId` only as last resort
|
||||
|
||||
### Async
|
||||
|
||||
- [ ] All async tests use `async/await`
|
||||
- [ ] `waitFor` wraps async assertions
|
||||
- [ ] Fake timers properly setup/teardown
|
||||
- [ ] No floating promises
|
||||
|
||||
### TypeScript
|
||||
|
||||
- [ ] No `any` types without justification
|
||||
- [ ] Mock data uses actual types from source
|
||||
- [ ] Factory functions have proper return types
|
||||
|
||||
## Coverage Goals (Per File)
|
||||
|
||||
For the current file being tested:
|
||||
|
||||
- [ ] 100% function coverage
|
||||
- [ ] 100% statement coverage
|
||||
- [ ] >95% branch coverage
|
||||
- [ ] >95% line coverage
|
||||
|
||||
## Post-Generation (Per File)
|
||||
|
||||
**Run these checks after EACH test file, not just at the end:**
|
||||
|
||||
- [ ] Run `pnpm test -- path/to/file.spec.tsx` - **MUST PASS before next file**
|
||||
- [ ] Fix any failures immediately
|
||||
- [ ] Mark file as complete in todo list
|
||||
- [ ] Only then proceed to next file
|
||||
|
||||
### After All Files Complete
|
||||
|
||||
- [ ] Run full directory test: `pnpm test -- path/to/directory/`
|
||||
- [ ] Check coverage report: `pnpm test -- --coverage`
|
||||
- [ ] Run `pnpm lint:fix` on all test files
|
||||
- [ ] Run `pnpm type-check:tsgo`
|
||||
|
||||
## Common Issues to Watch
|
||||
|
||||
### False Positives
|
||||
|
||||
```typescript
|
||||
// ❌ Mock doesn't match actual behavior
|
||||
jest.mock('./Component', () => () => <div>Mocked</div>)
|
||||
|
||||
// ✅ Mock matches actual conditional logic
|
||||
jest.mock('./Component', () => ({ isOpen }: any) =>
|
||||
isOpen ? <div>Content</div> : null
|
||||
)
|
||||
```
|
||||
|
||||
### State Leakage
|
||||
|
||||
```typescript
|
||||
// ❌ Shared state not reset
|
||||
let mockState = false
|
||||
jest.mock('./useHook', () => () => mockState)
|
||||
|
||||
// ✅ Reset in beforeEach
|
||||
beforeEach(() => {
|
||||
mockState = false
|
||||
})
|
||||
```
|
||||
|
||||
### Async Race Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Not awaited
|
||||
it('loads data', () => {
|
||||
render(<Component />)
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ✅ Properly awaited
|
||||
it('loads data', async () => {
|
||||
render(<Component />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Missing Edge Cases
|
||||
|
||||
Always test these scenarios:
|
||||
|
||||
- `null` / `undefined` inputs
|
||||
- Empty strings / arrays / objects
|
||||
- Boundary values (0, -1, MAX_INT)
|
||||
- Error states
|
||||
- Loading states
|
||||
- Disabled states
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# Run specific test
|
||||
pnpm test -- path/to/file.spec.tsx
|
||||
|
||||
# Run with coverage
|
||||
pnpm test -- --coverage path/to/file.spec.tsx
|
||||
|
||||
# Watch mode
|
||||
pnpm test -- --watch path/to/file.spec.tsx
|
||||
|
||||
# Update snapshots (use sparingly)
|
||||
pnpm test -- -u path/to/file.spec.tsx
|
||||
|
||||
# Analyze component
|
||||
pnpm analyze-component path/to/component.tsx
|
||||
|
||||
# Review existing test
|
||||
pnpm analyze-component path/to/component.tsx --review
|
||||
```
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
---
|
||||
name: Dify Frontend Testing
|
||||
description: Generate Jest + React Testing Library tests for Dify frontend components, hooks, and utilities. Triggers on testing, spec files, coverage, Jest, RTL, unit tests, integration tests, or write/review test requests.
|
||||
---
|
||||
|
||||
# Dify Frontend Testing Skill
|
||||
|
||||
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
||||
|
||||
> **⚠️ Authoritative Source**: This skill is derived from `web/testing/testing.md`. When in doubt, always refer to that document as the canonical specification.
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
Apply this skill when the user:
|
||||
|
||||
- Asks to **write tests** for a component, hook, or utility
|
||||
- Asks to **review existing tests** for completeness
|
||||
- Mentions **Jest**, **React Testing Library**, **RTL**, or **spec files**
|
||||
- Requests **test coverage** improvement
|
||||
- Uses `pnpm analyze-component` output as context
|
||||
- Mentions **testing**, **unit tests**, or **integration tests** for frontend code
|
||||
- Wants to understand **testing patterns** in the Dify codebase
|
||||
|
||||
**Do NOT apply** when:
|
||||
|
||||
- User is asking about backend/API tests (Python/pytest)
|
||||
- User is asking about E2E tests (Playwright/Cypress)
|
||||
- User is only asking conceptual questions without code context
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Tool | Version | Purpose |
|
||||
|------|---------|---------|
|
||||
| Jest | 29.7 | Test runner |
|
||||
| React Testing Library | 16.0 | Component testing |
|
||||
| happy-dom | - | Test environment |
|
||||
| nock | 14.0 | HTTP mocking |
|
||||
| TypeScript | 5.x | Type safety |
|
||||
|
||||
### Key Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Watch mode
|
||||
pnpm test -- --watch
|
||||
|
||||
# Run specific file
|
||||
pnpm test -- path/to/file.spec.tsx
|
||||
|
||||
# Generate coverage report
|
||||
pnpm test -- --coverage
|
||||
|
||||
# Analyze component complexity
|
||||
pnpm analyze-component <path>
|
||||
|
||||
# Review existing test
|
||||
pnpm analyze-component <path> --review
|
||||
```
|
||||
|
||||
### File Naming
|
||||
|
||||
- Test files: `ComponentName.spec.tsx` (same directory as component)
|
||||
- Integration tests: `web/__tests__/` directory
|
||||
|
||||
## Test Structure Template
|
||||
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import Component from './index'
|
||||
|
||||
// ✅ Import real project components (DO NOT mock these)
|
||||
// import Loading from '@/app/components/base/loading'
|
||||
// import { ChildComponent } from './child-component'
|
||||
|
||||
// ✅ Mock external dependencies only
|
||||
jest.mock('@/service/api')
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: jest.fn() }),
|
||||
usePathname: () => '/test',
|
||||
}))
|
||||
|
||||
// Shared state for mocks (if needed)
|
||||
let mockSharedState = false
|
||||
|
||||
describe('ComponentName', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks() // ✅ Reset mocks BEFORE each test
|
||||
mockSharedState = false // ✅ Reset shared state
|
||||
})
|
||||
|
||||
// Rendering tests (REQUIRED)
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = { title: 'Test' }
|
||||
|
||||
// Act
|
||||
render(<Component {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props tests (REQUIRED)
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<Component className="custom" />)
|
||||
expect(screen.getByRole('button')).toHaveClass('custom')
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions
|
||||
describe('User Interactions', () => {
|
||||
it('should handle click events', () => {
|
||||
const handleClick = jest.fn()
|
||||
render(<Component onClick={handleClick} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases (REQUIRED)
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle null data', () => {
|
||||
render(<Component data={null} />)
|
||||
expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
render(<Component items={[]} />)
|
||||
expect(screen.getByText(/empty/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Workflow (CRITICAL)
|
||||
|
||||
### ⚠️ Incremental Approach Required
|
||||
|
||||
**NEVER generate all test files at once.** For complex components or multi-file directories:
|
||||
|
||||
1. **Analyze & Plan**: List all files, order by complexity (simple → complex)
|
||||
1. **Process ONE at a time**: Write test → Run test → Fix if needed → Next
|
||||
1. **Verify before proceeding**: Do NOT continue to next file until current passes
|
||||
|
||||
```
|
||||
For each file:
|
||||
┌────────────────────────────────────────┐
|
||||
│ 1. Write test │
|
||||
│ 2. Run: pnpm test -- <file>.spec.tsx │
|
||||
│ 3. PASS? → Mark complete, next file │
|
||||
│ FAIL? → Fix first, then continue │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Complexity-Based Order
|
||||
|
||||
Process in this order for multi-file testing:
|
||||
|
||||
1. 🟢 Utility functions (simplest)
|
||||
1. 🟢 Custom hooks
|
||||
1. 🟡 Simple components (presentational)
|
||||
1. 🟡 Medium components (state, effects)
|
||||
1. 🔴 Complex components (API, routing)
|
||||
1. 🔴 Integration tests (index files - last)
|
||||
|
||||
### When to Refactor First
|
||||
|
||||
- **Complexity > 50**: Break into smaller pieces before testing
|
||||
- **500+ lines**: Consider splitting before testing
|
||||
- **Many dependencies**: Extract logic into hooks first
|
||||
|
||||
> 📖 See `guides/workflow.md` for complete workflow details and todo list format.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Path-Level Testing (Directory Testing)
|
||||
|
||||
When assigned to test a directory/path, test **ALL content** within that path:
|
||||
|
||||
- Test all components, hooks, utilities in the directory (not just `index` file)
|
||||
- Use incremental approach: one file at a time, verify each before proceeding
|
||||
- Goal: 100% coverage of ALL files in the directory
|
||||
|
||||
### Integration Testing First
|
||||
|
||||
**Prefer integration testing** when writing tests for a directory:
|
||||
|
||||
- ✅ **Import real project components** directly (including base components and siblings)
|
||||
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
||||
- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
|
||||
- ❌ **DO NOT mock** sibling/child components in the same directory
|
||||
|
||||
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. AAA Pattern (Arrange-Act-Assert)
|
||||
|
||||
Every test should clearly separate:
|
||||
|
||||
- **Arrange**: Setup test data and render component
|
||||
- **Act**: Perform user actions
|
||||
- **Assert**: Verify expected outcomes
|
||||
|
||||
### 2. Black-Box Testing
|
||||
|
||||
- Test observable behavior, not implementation details
|
||||
- Use semantic queries (getByRole, getByLabelText)
|
||||
- Avoid testing internal state directly
|
||||
- **Prefer pattern matching over hardcoded strings** in assertions:
|
||||
|
||||
```typescript
|
||||
// ❌ Avoid: hardcoded text assertions
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
|
||||
// ✅ Better: role-based queries
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
|
||||
// ✅ Better: pattern matching
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### 3. Single Behavior Per Test
|
||||
|
||||
Each test verifies ONE user-observable behavior:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: One behavior
|
||||
it('should disable button when loading', () => {
|
||||
render(<Button loading />)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
// ❌ Bad: Multiple behaviors
|
||||
it('should handle loading state', () => {
|
||||
render(<Button loading />)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button')).toHaveClass('loading')
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Semantic Naming
|
||||
|
||||
Use `should <behavior> when <condition>`:
|
||||
|
||||
```typescript
|
||||
it('should show error message when validation fails')
|
||||
it('should call onSubmit when form is valid')
|
||||
it('should disable input when isReadOnly is true')
|
||||
```
|
||||
|
||||
## Required Test Scenarios
|
||||
|
||||
### Always Required (All Components)
|
||||
|
||||
1. **Rendering**: Component renders without crashing
|
||||
1. **Props**: Required props, optional props, default values
|
||||
1. **Edge Cases**: null, undefined, empty values, boundary conditions
|
||||
|
||||
### Conditional (When Present)
|
||||
|
||||
| Feature | Test Focus |
|
||||
|---------|-----------|
|
||||
| `useState` | Initial state, transitions, cleanup |
|
||||
| `useEffect` | Execution, dependencies, cleanup |
|
||||
| Event handlers | All onClick, onChange, onSubmit, keyboard |
|
||||
| API calls | Loading, success, error states |
|
||||
| Routing | Navigation, params, query strings |
|
||||
| `useCallback`/`useMemo` | Referential equality |
|
||||
| Context | Provider values, consumer behavior |
|
||||
| Forms | Validation, submission, error display |
|
||||
|
||||
## Coverage Goals (Per File)
|
||||
|
||||
For each test file generated, aim for:
|
||||
|
||||
- ✅ **100%** function coverage
|
||||
- ✅ **100%** statement coverage
|
||||
- ✅ **>95%** branch coverage
|
||||
- ✅ **>95%** line coverage
|
||||
|
||||
> **Note**: For multi-file directories, process one file at a time with full coverage each. See `guides/workflow.md`.
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
For more detailed information, refer to:
|
||||
|
||||
- `guides/workflow.md` - **Incremental testing workflow** (MUST READ for multi-file testing)
|
||||
- `guides/mocking.md` - Mock patterns and best practices
|
||||
- `guides/async-testing.md` - Async operations and API calls
|
||||
- `guides/domain-components.md` - Workflow, Dataset, Configuration testing
|
||||
- `guides/common-patterns.md` - Frequently used testing patterns
|
||||
|
||||
## Authoritative References
|
||||
|
||||
### Primary Specification (MUST follow)
|
||||
|
||||
- **`web/testing/testing.md`** - The canonical testing specification. This skill is derived from this document.
|
||||
|
||||
### Reference Examples in Codebase
|
||||
|
||||
- `web/utils/classnames.spec.ts` - Utility function tests
|
||||
- `web/app/components/base/button/index.spec.tsx` - Component tests
|
||||
- `web/__mocks__/provider-context.ts` - Mock factory example
|
||||
|
||||
### Project Configuration
|
||||
|
||||
- `web/jest.config.ts` - Jest configuration
|
||||
- `web/jest.setup.ts` - Test environment setup
|
||||
- `web/testing/analyze-component.js` - Component analysis tool
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
# Async Testing Guide
|
||||
|
||||
## Core Async Patterns
|
||||
|
||||
### 1. waitFor - Wait for Condition
|
||||
|
||||
```typescript
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
|
||||
it('should load and display data', async () => {
|
||||
render(<DataComponent />)
|
||||
|
||||
// Wait for element to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Loaded Data')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide loading spinner after load', async () => {
|
||||
render(<DataComponent />)
|
||||
|
||||
// Wait for element to disappear
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 2. findBy\* - Async Queries
|
||||
|
||||
```typescript
|
||||
it('should show user name after fetch', async () => {
|
||||
render(<UserProfile />)
|
||||
|
||||
// findBy returns a promise, auto-waits up to 1000ms
|
||||
const userName = await screen.findByText('John Doe')
|
||||
expect(userName).toBeInTheDocument()
|
||||
|
||||
// findByRole with options
|
||||
const button = await screen.findByRole('button', { name: /submit/i })
|
||||
expect(button).toBeEnabled()
|
||||
})
|
||||
```
|
||||
|
||||
### 3. userEvent for Async Interactions
|
||||
|
||||
```typescript
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
it('should submit form', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = jest.fn()
|
||||
|
||||
render(<Form onSubmit={onSubmit} />)
|
||||
|
||||
// userEvent methods are async
|
||||
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
||||
await user.click(screen.getByRole('button', { name: /submit/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' })
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Fake Timers
|
||||
|
||||
### When to Use Fake Timers
|
||||
|
||||
- Testing components with `setTimeout`/`setInterval`
|
||||
- Testing debounce/throttle behavior
|
||||
- Testing animations or delayed transitions
|
||||
- Testing polling or retry logic
|
||||
|
||||
### Basic Fake Timer Setup
|
||||
|
||||
```typescript
|
||||
describe('Debounced Search', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should debounce search input', async () => {
|
||||
const onSearch = jest.fn()
|
||||
render(<SearchInput onSearch={onSearch} debounceMs={300} />)
|
||||
|
||||
// Type in the input
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } })
|
||||
|
||||
// Search not called immediately
|
||||
expect(onSearch).not.toHaveBeenCalled()
|
||||
|
||||
// Advance timers
|
||||
jest.advanceTimersByTime(300)
|
||||
|
||||
// Now search is called
|
||||
expect(onSearch).toHaveBeenCalledWith('query')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Fake Timers with Async Code
|
||||
|
||||
```typescript
|
||||
it('should retry on failure', async () => {
|
||||
jest.useFakeTimers()
|
||||
const fetchData = jest.fn()
|
||||
.mockRejectedValueOnce(new Error('Network error'))
|
||||
.mockResolvedValueOnce({ data: 'success' })
|
||||
|
||||
render(<RetryComponent fetchData={fetchData} retryDelayMs={1000} />)
|
||||
|
||||
// First call fails
|
||||
await waitFor(() => {
|
||||
expect(fetchData).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Advance timer for retry
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
// Second call succeeds
|
||||
await waitFor(() => {
|
||||
expect(fetchData).toHaveBeenCalledTimes(2)
|
||||
expect(screen.getByText('success')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
jest.useRealTimers()
|
||||
})
|
||||
```
|
||||
|
||||
### Common Fake Timer Utilities
|
||||
|
||||
```typescript
|
||||
// Run all pending timers
|
||||
jest.runAllTimers()
|
||||
|
||||
// Run only pending timers (not new ones created during execution)
|
||||
jest.runOnlyPendingTimers()
|
||||
|
||||
// Advance by specific time
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
// Get current fake time
|
||||
jest.now()
|
||||
|
||||
// Clear all timers
|
||||
jest.clearAllTimers()
|
||||
```
|
||||
|
||||
## API Testing Patterns
|
||||
|
||||
### Loading → Success → Error States
|
||||
|
||||
```typescript
|
||||
describe('DataFetcher', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should show loading state', () => {
|
||||
mockedApi.fetchData.mockImplementation(() => new Promise(() => {})) // Never resolves
|
||||
|
||||
render(<DataFetcher />)
|
||||
|
||||
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show data on success', async () => {
|
||||
mockedApi.fetchData.mockResolvedValue({ items: ['Item 1', 'Item 2'] })
|
||||
|
||||
render(<DataFetcher />)
|
||||
|
||||
// Use findBy* for multiple async elements (better error messages than waitFor with multiple assertions)
|
||||
const item1 = await screen.findByText('Item 1')
|
||||
const item2 = await screen.findByText('Item 2')
|
||||
expect(item1).toBeInTheDocument()
|
||||
expect(item2).toBeInTheDocument()
|
||||
|
||||
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error on failure', async () => {
|
||||
mockedApi.fetchData.mockRejectedValue(new Error('Failed to fetch'))
|
||||
|
||||
render(<DataFetcher />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to fetch/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should retry on error', async () => {
|
||||
mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
render(<DataFetcher />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
mockedApi.fetchData.mockResolvedValue({ items: ['Item 1'] })
|
||||
fireEvent.click(screen.getByRole('button', { name: /retry/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Mutations
|
||||
|
||||
```typescript
|
||||
it('should submit form and show success', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedApi.createItem.mockResolvedValue({ id: '1', name: 'New Item' })
|
||||
|
||||
render(<CreateItemForm />)
|
||||
|
||||
await user.type(screen.getByLabelText('Name'), 'New Item')
|
||||
await user.click(screen.getByRole('button', { name: /create/i }))
|
||||
|
||||
// Button should be disabled during submission
|
||||
expect(screen.getByRole('button', { name: /creating/i })).toBeDisabled()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/created successfully/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(mockedApi.createItem).toHaveBeenCalledWith({ name: 'New Item' })
|
||||
})
|
||||
```
|
||||
|
||||
## useEffect Testing
|
||||
|
||||
### Testing Effect Execution
|
||||
|
||||
```typescript
|
||||
it('should fetch data on mount', async () => {
|
||||
const fetchData = jest.fn().mockResolvedValue({ data: 'test' })
|
||||
|
||||
render(<ComponentWithEffect fetchData={fetchData} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchData).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Effect Dependencies
|
||||
|
||||
```typescript
|
||||
it('should refetch when id changes', async () => {
|
||||
const fetchData = jest.fn().mockResolvedValue({ data: 'test' })
|
||||
|
||||
const { rerender } = render(<ComponentWithEffect id="1" fetchData={fetchData} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchData).toHaveBeenCalledWith('1')
|
||||
})
|
||||
|
||||
rerender(<ComponentWithEffect id="2" fetchData={fetchData} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchData).toHaveBeenCalledWith('2')
|
||||
expect(fetchData).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Effect Cleanup
|
||||
|
||||
```typescript
|
||||
it('should cleanup subscription on unmount', () => {
|
||||
const subscribe = jest.fn()
|
||||
const unsubscribe = jest.fn()
|
||||
subscribe.mockReturnValue(unsubscribe)
|
||||
|
||||
const { unmount } = render(<SubscriptionComponent subscribe={subscribe} />)
|
||||
|
||||
expect(subscribe).toHaveBeenCalledTimes(1)
|
||||
|
||||
unmount()
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
```
|
||||
|
||||
## Common Async Pitfalls
|
||||
|
||||
### ❌ Don't: Forget to await
|
||||
|
||||
```typescript
|
||||
// Bad - test may pass even if assertion fails
|
||||
it('should load data', () => {
|
||||
render(<Component />)
|
||||
waitFor(() => {
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Good - properly awaited
|
||||
it('should load data', async () => {
|
||||
render(<Component />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### ❌ Don't: Use multiple assertions in single waitFor
|
||||
|
||||
```typescript
|
||||
// Bad - if first assertion fails, won't know about second
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Good - separate waitFor or use findBy
|
||||
const title = await screen.findByText('Title')
|
||||
const description = await screen.findByText('Description')
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### ❌ Don't: Mix fake timers with real async
|
||||
|
||||
```typescript
|
||||
// Bad - fake timers don't work well with real Promises
|
||||
jest.useFakeTimers()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
}) // May timeout!
|
||||
|
||||
// Good - use runAllTimers or advanceTimersByTime
|
||||
jest.useFakeTimers()
|
||||
render(<Component />)
|
||||
jest.runAllTimers()
|
||||
expect(screen.getByText('Data')).toBeInTheDocument()
|
||||
```
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
# Common Testing Patterns
|
||||
|
||||
## Query Priority
|
||||
|
||||
Use queries in this order (most to least preferred):
|
||||
|
||||
```typescript
|
||||
// 1. getByRole - Most recommended (accessibility)
|
||||
screen.getByRole('button', { name: /submit/i })
|
||||
screen.getByRole('textbox', { name: /email/i })
|
||||
screen.getByRole('heading', { level: 1 })
|
||||
|
||||
// 2. getByLabelText - Form fields
|
||||
screen.getByLabelText('Email address')
|
||||
screen.getByLabelText(/password/i)
|
||||
|
||||
// 3. getByPlaceholderText - When no label
|
||||
screen.getByPlaceholderText('Search...')
|
||||
|
||||
// 4. getByText - Non-interactive elements
|
||||
screen.getByText('Welcome to Dify')
|
||||
screen.getByText(/loading/i)
|
||||
|
||||
// 5. getByDisplayValue - Current input value
|
||||
screen.getByDisplayValue('current value')
|
||||
|
||||
// 6. getByAltText - Images
|
||||
screen.getByAltText('Company logo')
|
||||
|
||||
// 7. getByTitle - Tooltip elements
|
||||
screen.getByTitle('Close')
|
||||
|
||||
// 8. getByTestId - Last resort only!
|
||||
screen.getByTestId('custom-element')
|
||||
```
|
||||
|
||||
## Event Handling Patterns
|
||||
|
||||
### Click Events
|
||||
|
||||
```typescript
|
||||
// Basic click
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// With userEvent (preferred for realistic interaction)
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
// Double click
|
||||
await user.dblClick(screen.getByRole('button'))
|
||||
|
||||
// Right click
|
||||
await user.pointer({ keys: '[MouseRight]', target: screen.getByRole('button') })
|
||||
```
|
||||
|
||||
### Form Input
|
||||
|
||||
```typescript
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Type in input
|
||||
await user.type(screen.getByRole('textbox'), 'Hello World')
|
||||
|
||||
// Clear and type
|
||||
await user.clear(screen.getByRole('textbox'))
|
||||
await user.type(screen.getByRole('textbox'), 'New value')
|
||||
|
||||
// Select option
|
||||
await user.selectOptions(screen.getByRole('combobox'), 'option-value')
|
||||
|
||||
// Check checkbox
|
||||
await user.click(screen.getByRole('checkbox'))
|
||||
|
||||
// Upload file
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
await user.upload(screen.getByLabelText(/upload/i), file)
|
||||
```
|
||||
|
||||
### Keyboard Events
|
||||
|
||||
```typescript
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Press Enter
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
// Press Escape
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
// Keyboard shortcut
|
||||
await user.keyboard('{Control>}a{/Control}') // Ctrl+A
|
||||
|
||||
// Tab navigation
|
||||
await user.tab()
|
||||
|
||||
// Arrow keys
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await user.keyboard('{ArrowUp}')
|
||||
```
|
||||
|
||||
## Component State Testing
|
||||
|
||||
### Testing State Transitions
|
||||
|
||||
```typescript
|
||||
describe('Counter', () => {
|
||||
it('should increment count', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Counter initialCount={0} />)
|
||||
|
||||
// Initial state
|
||||
expect(screen.getByText('Count: 0')).toBeInTheDocument()
|
||||
|
||||
// Trigger transition
|
||||
await user.click(screen.getByRole('button', { name: /increment/i }))
|
||||
|
||||
// New state
|
||||
expect(screen.getByText('Count: 1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Controlled Components
|
||||
|
||||
```typescript
|
||||
describe('ControlledInput', () => {
|
||||
it('should call onChange with new value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleChange = jest.fn()
|
||||
|
||||
render(<ControlledInput value="" onChange={handleChange} />)
|
||||
|
||||
await user.type(screen.getByRole('textbox'), 'a')
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith('a')
|
||||
})
|
||||
|
||||
it('should display controlled value', () => {
|
||||
render(<ControlledInput value="controlled" onChange={jest.fn()} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('controlled')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Conditional Rendering Testing
|
||||
|
||||
```typescript
|
||||
describe('ConditionalComponent', () => {
|
||||
it('should show loading state', () => {
|
||||
render(<DataDisplay isLoading={true} data={null} />)
|
||||
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('data-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error state', () => {
|
||||
render(<DataDisplay isLoading={false} data={null} error="Failed to load" />)
|
||||
|
||||
expect(screen.getByText(/failed to load/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show data when loaded', () => {
|
||||
render(<DataDisplay isLoading={false} data={{ name: 'Test' }} />)
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show empty state when no data', () => {
|
||||
render(<DataDisplay isLoading={false} data={[]} />)
|
||||
|
||||
expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## List Rendering Testing
|
||||
|
||||
```typescript
|
||||
describe('ItemList', () => {
|
||||
const items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
]
|
||||
|
||||
it('should render all items', () => {
|
||||
render(<ItemList items={items} />)
|
||||
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(3)
|
||||
items.forEach(item => {
|
||||
expect(screen.getByText(item.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle item selection', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = jest.fn()
|
||||
|
||||
render(<ItemList items={items} onSelect={onSelect} />)
|
||||
|
||||
await user.click(screen.getByText('Item 2'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(items[1])
|
||||
})
|
||||
|
||||
it('should handle empty list', () => {
|
||||
render(<ItemList items={[]} />)
|
||||
|
||||
expect(screen.getByText(/no items/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Modal/Dialog Testing
|
||||
|
||||
```typescript
|
||||
describe('Modal', () => {
|
||||
it('should not render when closed', () => {
|
||||
render(<Modal isOpen={false} onClose={jest.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when open', () => {
|
||||
render(<Modal isOpen={true} onClose={jest.fn()} />)
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClose when clicking overlay', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleClose = jest.fn()
|
||||
|
||||
render(<Modal isOpen={true} onClose={handleClose} />)
|
||||
|
||||
await user.click(screen.getByTestId('modal-overlay'))
|
||||
|
||||
expect(handleClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onClose when pressing Escape', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleClose = jest.fn()
|
||||
|
||||
render(<Modal isOpen={true} onClose={handleClose} />)
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
expect(handleClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trap focus inside modal', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<Modal isOpen={true} onClose={jest.fn()}>
|
||||
<button>First</button>
|
||||
<button>Second</button>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
// Focus should cycle within modal
|
||||
await user.tab()
|
||||
expect(screen.getByText('First')).toHaveFocus()
|
||||
|
||||
await user.tab()
|
||||
expect(screen.getByText('Second')).toHaveFocus()
|
||||
|
||||
await user.tab()
|
||||
expect(screen.getByText('First')).toHaveFocus() // Cycles back
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Form Testing
|
||||
|
||||
```typescript
|
||||
describe('LoginForm', () => {
|
||||
it('should submit valid form', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = jest.fn()
|
||||
|
||||
render(<LoginForm onSubmit={onSubmit} />)
|
||||
|
||||
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
|
||||
await user.type(screen.getByLabelText(/password/i), 'password123')
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
})
|
||||
})
|
||||
|
||||
it('should show validation errors', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<LoginForm onSubmit={jest.fn()} />)
|
||||
|
||||
// Submit empty form
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
|
||||
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/password is required/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should validate email format', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<LoginForm onSubmit={jest.fn()} />)
|
||||
|
||||
await user.type(screen.getByLabelText(/email/i), 'invalid-email')
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
|
||||
expect(screen.getByText(/invalid email/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable submit button while submitting', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = jest.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
|
||||
render(<LoginForm onSubmit={onSubmit} />)
|
||||
|
||||
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
|
||||
await user.type(screen.getByLabelText(/password/i), 'password123')
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
||||
|
||||
expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /sign in/i })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Data-Driven Tests with test.each
|
||||
|
||||
```typescript
|
||||
describe('StatusBadge', () => {
|
||||
test.each([
|
||||
['success', 'bg-green-500'],
|
||||
['warning', 'bg-yellow-500'],
|
||||
['error', 'bg-red-500'],
|
||||
['info', 'bg-blue-500'],
|
||||
])('should apply correct class for %s status', (status, expectedClass) => {
|
||||
render(<StatusBadge status={status} />)
|
||||
|
||||
expect(screen.getByTestId('status-badge')).toHaveClass(expectedClass)
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ input: null, expected: 'Unknown' },
|
||||
{ input: undefined, expected: 'Unknown' },
|
||||
{ input: '', expected: 'Unknown' },
|
||||
{ input: 'invalid', expected: 'Unknown' },
|
||||
])('should show "Unknown" for invalid input: $input', ({ input, expected }) => {
|
||||
render(<StatusBadge status={input} />)
|
||||
|
||||
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Print entire DOM
|
||||
screen.debug()
|
||||
|
||||
// Print specific element
|
||||
screen.debug(screen.getByRole('button'))
|
||||
|
||||
// Log testing playground URL
|
||||
screen.logTestingPlaygroundURL()
|
||||
|
||||
// Pretty print DOM
|
||||
import { prettyDOM } from '@testing-library/react'
|
||||
console.log(prettyDOM(screen.getByRole('dialog')))
|
||||
|
||||
// Check available roles
|
||||
import { getRoles } from '@testing-library/react'
|
||||
console.log(getRoles(container))
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### ❌ Don't Use Implementation Details
|
||||
|
||||
```typescript
|
||||
// Bad - testing implementation
|
||||
expect(component.state.isOpen).toBe(true)
|
||||
expect(wrapper.find('.internal-class').length).toBe(1)
|
||||
|
||||
// Good - testing behavior
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### ❌ Don't Forget Cleanup
|
||||
|
||||
```typescript
|
||||
// Bad - may leak state between tests
|
||||
it('test 1', () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
// Good - cleanup is automatic with RTL, but reset mocks
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
```
|
||||
|
||||
### ❌ Don't Use Exact String Matching (Prefer Black-Box Assertions)
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - hardcoded strings are brittle
|
||||
expect(screen.getByText('Submit Form')).toBeInTheDocument()
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
|
||||
// ✅ Good - role-based queries (most semantic)
|
||||
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
|
||||
// ✅ Good - pattern matching (flexible)
|
||||
expect(screen.getByText(/submit/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
||||
|
||||
// ✅ Good - test behavior, not exact UI text
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
**Why prefer black-box assertions?**
|
||||
|
||||
- Text content may change (i18n, copy updates)
|
||||
- Role-based queries test accessibility
|
||||
- Pattern matching is resilient to minor changes
|
||||
- Tests focus on behavior, not implementation details
|
||||
|
||||
### ❌ Don't Assert on Absence Without Query
|
||||
|
||||
```typescript
|
||||
// Bad - throws if not found
|
||||
expect(screen.getByText('Error')).not.toBeInTheDocument() // Error!
|
||||
|
||||
// Good - use queryBy for absence assertions
|
||||
expect(screen.queryByText('Error')).not.toBeInTheDocument()
|
||||
```
|
||||
|
|
@ -0,0 +1,523 @@
|
|||
# Domain-Specific Component Testing
|
||||
|
||||
This guide covers testing patterns for Dify's domain-specific components.
|
||||
|
||||
## Workflow Components (`workflow/`)
|
||||
|
||||
Workflow components handle node configuration, data flow, and graph operations.
|
||||
|
||||
### Key Test Areas
|
||||
|
||||
1. **Node Configuration**
|
||||
1. **Data Validation**
|
||||
1. **Variable Passing**
|
||||
1. **Edge Connections**
|
||||
1. **Error Handling**
|
||||
|
||||
### Example: Node Configuration Panel
|
||||
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import NodeConfigPanel from './node-config-panel'
|
||||
import { createMockNode, createMockWorkflowContext } from '@/__mocks__/workflow'
|
||||
|
||||
// Mock workflow context
|
||||
jest.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowStore: () => mockWorkflowStore,
|
||||
useNodesInteractions: () => mockNodesInteractions,
|
||||
}))
|
||||
|
||||
let mockWorkflowStore = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
updateNode: jest.fn(),
|
||||
}
|
||||
|
||||
let mockNodesInteractions = {
|
||||
handleNodeSelect: jest.fn(),
|
||||
handleNodeDelete: jest.fn(),
|
||||
}
|
||||
|
||||
describe('NodeConfigPanel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockWorkflowStore = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
updateNode: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('Node Configuration', () => {
|
||||
it('should render node type selector', () => {
|
||||
const node = createMockNode({ type: 'llm' })
|
||||
render(<NodeConfigPanel node={node} />)
|
||||
|
||||
expect(screen.getByLabelText(/model/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update node config on change', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createMockNode({ type: 'llm' })
|
||||
|
||||
render(<NodeConfigPanel node={node} />)
|
||||
|
||||
await user.selectOptions(screen.getByLabelText(/model/i), 'gpt-4')
|
||||
|
||||
expect(mockWorkflowStore.updateNode).toHaveBeenCalledWith(
|
||||
node.id,
|
||||
expect.objectContaining({ model: 'gpt-4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Validation', () => {
|
||||
it('should show error for invalid input', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createMockNode({ type: 'code' })
|
||||
|
||||
render(<NodeConfigPanel node={node} />)
|
||||
|
||||
// Enter invalid code
|
||||
const codeInput = screen.getByLabelText(/code/i)
|
||||
await user.clear(codeInput)
|
||||
await user.type(codeInput, 'invalid syntax {{{')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/syntax error/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const node = createMockNode({ type: 'http', data: { url: '' } })
|
||||
|
||||
render(<NodeConfigPanel node={node} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/url is required/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Variable Passing', () => {
|
||||
it('should display available variables from upstream nodes', () => {
|
||||
const upstreamNode = createMockNode({
|
||||
id: 'node-1',
|
||||
type: 'start',
|
||||
data: { outputs: [{ name: 'user_input', type: 'string' }] },
|
||||
})
|
||||
const currentNode = createMockNode({
|
||||
id: 'node-2',
|
||||
type: 'llm',
|
||||
})
|
||||
|
||||
mockWorkflowStore.nodes = [upstreamNode, currentNode]
|
||||
mockWorkflowStore.edges = [{ source: 'node-1', target: 'node-2' }]
|
||||
|
||||
render(<NodeConfigPanel node={currentNode} />)
|
||||
|
||||
// Variable selector should show upstream variables
|
||||
fireEvent.click(screen.getByRole('button', { name: /add variable/i }))
|
||||
|
||||
expect(screen.getByText('user_input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should insert variable into prompt template', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createMockNode({ type: 'llm' })
|
||||
|
||||
render(<NodeConfigPanel node={node} />)
|
||||
|
||||
// Click variable button
|
||||
await user.click(screen.getByRole('button', { name: /insert variable/i }))
|
||||
await user.click(screen.getByText('user_input'))
|
||||
|
||||
const promptInput = screen.getByLabelText(/prompt/i)
|
||||
expect(promptInput).toHaveValue(expect.stringContaining('{{user_input}}'))
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Dataset Components (`dataset/`)
|
||||
|
||||
Dataset components handle file uploads, data display, and search/filter operations.
|
||||
|
||||
### Key Test Areas
|
||||
|
||||
1. **File Upload**
|
||||
1. **File Type Validation**
|
||||
1. **Pagination**
|
||||
1. **Search & Filtering**
|
||||
1. **Data Format Handling**
|
||||
|
||||
### Example: Document Uploader
|
||||
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import DocumentUploader from './document-uploader'
|
||||
|
||||
jest.mock('@/service/datasets', () => ({
|
||||
uploadDocument: jest.fn(),
|
||||
parseDocument: jest.fn(),
|
||||
}))
|
||||
|
||||
import * as datasetService from '@/service/datasets'
|
||||
const mockedService = datasetService as jest.Mocked<typeof datasetService>
|
||||
|
||||
describe('DocumentUploader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('File Upload', () => {
|
||||
it('should accept valid file types', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onUpload = jest.fn()
|
||||
mockedService.uploadDocument.mockResolvedValue({ id: 'doc-1' })
|
||||
|
||||
render(<DocumentUploader onUpload={onUpload} />)
|
||||
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
const input = screen.getByLabelText(/upload/i)
|
||||
|
||||
await user.upload(input, file)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedService.uploadDocument).toHaveBeenCalledWith(
|
||||
expect.any(FormData)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject invalid file types', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<DocumentUploader />)
|
||||
|
||||
const file = new File(['content'], 'test.exe', { type: 'application/x-msdownload' })
|
||||
const input = screen.getByLabelText(/upload/i)
|
||||
|
||||
await user.upload(input, file)
|
||||
|
||||
expect(screen.getByText(/unsupported file type/i)).toBeInTheDocument()
|
||||
expect(mockedService.uploadDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show upload progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Mock upload with progress
|
||||
mockedService.uploadDocument.mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve({ id: 'doc-1' }), 100)
|
||||
})
|
||||
})
|
||||
|
||||
render(<DocumentUploader />)
|
||||
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
await user.upload(screen.getByLabelText(/upload/i), file)
|
||||
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle upload failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.uploadDocument.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
render(<DocumentUploader />)
|
||||
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
await user.upload(screen.getByLabelText(/upload/i), file)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/upload failed/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow retry after failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.uploadDocument
|
||||
.mockRejectedValueOnce(new Error('Network error'))
|
||||
.mockResolvedValueOnce({ id: 'doc-1' })
|
||||
|
||||
render(<DocumentUploader />)
|
||||
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
await user.upload(screen.getByLabelText(/upload/i), file)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /retry/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/uploaded successfully/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Example: Document List with Pagination
|
||||
|
||||
```typescript
|
||||
describe('DocumentList', () => {
|
||||
describe('Pagination', () => {
|
||||
it('should load first page on mount', async () => {
|
||||
mockedService.getDocuments.mockResolvedValue({
|
||||
data: [{ id: '1', name: 'Doc 1' }],
|
||||
total: 50,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
render(<DocumentList datasetId="ds-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Doc 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(mockedService.getDocuments).toHaveBeenCalledWith('ds-1', { page: 1 })
|
||||
})
|
||||
|
||||
it('should navigate to next page', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.getDocuments.mockResolvedValue({
|
||||
data: [{ id: '1', name: 'Doc 1' }],
|
||||
total: 50,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
render(<DocumentList datasetId="ds-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Doc 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
mockedService.getDocuments.mockResolvedValue({
|
||||
data: [{ id: '11', name: 'Doc 11' }],
|
||||
total: 50,
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /next/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Doc 11')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search & Filtering', () => {
|
||||
it('should filter by search query', async () => {
|
||||
const user = userEvent.setup()
|
||||
jest.useFakeTimers()
|
||||
|
||||
render(<DocumentList datasetId="ds-1" />)
|
||||
|
||||
await user.type(screen.getByPlaceholderText(/search/i), 'test query')
|
||||
|
||||
// Debounce
|
||||
jest.advanceTimersByTime(300)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedService.getDocuments).toHaveBeenCalledWith(
|
||||
'ds-1',
|
||||
expect.objectContaining({ search: 'test query' })
|
||||
)
|
||||
})
|
||||
|
||||
jest.useRealTimers()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration Components (`app/configuration/`, `config/`)
|
||||
|
||||
Configuration components handle forms, validation, and data persistence.
|
||||
|
||||
### Key Test Areas
|
||||
|
||||
1. **Form Validation**
|
||||
1. **Save/Reset**
|
||||
1. **Required vs Optional Fields**
|
||||
1. **Configuration Persistence**
|
||||
1. **Error Feedback**
|
||||
|
||||
### Example: App Configuration Form
|
||||
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import AppConfigForm from './app-config-form'
|
||||
|
||||
jest.mock('@/service/apps', () => ({
|
||||
updateAppConfig: jest.fn(),
|
||||
getAppConfig: jest.fn(),
|
||||
}))
|
||||
|
||||
import * as appService from '@/service/apps'
|
||||
const mockedService = appService as jest.Mocked<typeof appService>
|
||||
|
||||
describe('AppConfigForm', () => {
|
||||
const defaultConfig = {
|
||||
name: 'My App',
|
||||
description: '',
|
||||
icon: 'default',
|
||||
openingStatement: '',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockedService.getAppConfig.mockResolvedValue(defaultConfig)
|
||||
})
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('should require app name', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
// Clear name field
|
||||
await user.clear(screen.getByLabelText(/name/i))
|
||||
await user.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
expect(screen.getByText(/name is required/i)).toBeInTheDocument()
|
||||
expect(mockedService.updateAppConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should validate name length', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Enter very long name
|
||||
await user.clear(screen.getByLabelText(/name/i))
|
||||
await user.type(screen.getByLabelText(/name/i), 'a'.repeat(101))
|
||||
|
||||
expect(screen.getByText(/name must be less than 100 characters/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should allow empty optional fields', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.updateAppConfig.mockResolvedValue({ success: true })
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
// Leave description empty (optional)
|
||||
await user.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedService.updateAppConfig).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Save/Reset Functionality', () => {
|
||||
it('should save configuration', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.updateAppConfig.mockResolvedValue({ success: true })
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
await user.clear(screen.getByLabelText(/name/i))
|
||||
await user.type(screen.getByLabelText(/name/i), 'Updated App')
|
||||
await user.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedService.updateAppConfig).toHaveBeenCalledWith(
|
||||
'app-1',
|
||||
expect.objectContaining({ name: 'Updated App' })
|
||||
)
|
||||
})
|
||||
|
||||
expect(screen.getByText(/saved successfully/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset to default values', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
// Make changes
|
||||
await user.clear(screen.getByLabelText(/name/i))
|
||||
await user.type(screen.getByLabelText(/name/i), 'Changed Name')
|
||||
|
||||
// Reset
|
||||
await user.click(screen.getByRole('button', { name: /reset/i }))
|
||||
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
it('should show unsaved changes warning', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
// Make changes
|
||||
await user.type(screen.getByLabelText(/name/i), ' Updated')
|
||||
|
||||
expect(screen.getByText(/unsaved changes/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should show error on save failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockedService.updateAppConfig.mockRejectedValue(new Error('Server error'))
|
||||
|
||||
render(<AppConfigForm appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name/i)).toHaveValue('My App')
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to save/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
# Mocking Guide for Dify Frontend Tests
|
||||
|
||||
## ⚠️ Important: What NOT to Mock
|
||||
|
||||
### DO NOT Mock Base Components
|
||||
|
||||
**Never mock components from `@/app/components/base/`** such as:
|
||||
|
||||
- `Loading`, `Spinner`
|
||||
- `Button`, `Input`, `Select`
|
||||
- `Tooltip`, `Modal`, `Dropdown`
|
||||
- `Icon`, `Badge`, `Tag`
|
||||
|
||||
**Why?**
|
||||
|
||||
- Base components will have their own dedicated tests
|
||||
- Mocking them creates false positives (tests pass but real integration fails)
|
||||
- Using real components tests actual integration behavior
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG: Don't mock base components
|
||||
jest.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
||||
jest.mock('@/app/components/base/button', () => ({ children }: any) => <button>{children}</button>)
|
||||
|
||||
// ✅ CORRECT: Import and use real base components
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Button from '@/app/components/base/button'
|
||||
// They will render normally in tests
|
||||
```
|
||||
|
||||
### What TO Mock
|
||||
|
||||
Only mock these categories:
|
||||
|
||||
1. **API services** (`@/service/*`) - Network calls
|
||||
1. **Complex context providers** - When setup is too difficult
|
||||
1. **Third-party libraries with side effects** - `next/navigation`, external SDKs
|
||||
1. **i18n** - Always mock to return keys
|
||||
|
||||
## Mock Placement
|
||||
|
||||
| Location | Purpose |
|
||||
|----------|---------|
|
||||
| `web/__mocks__/` | Reusable mocks shared across multiple test files |
|
||||
| Test file | Test-specific mocks, inline with `jest.mock()` |
|
||||
|
||||
## Essential Mocks
|
||||
|
||||
### 1. i18n (Always Required)
|
||||
|
||||
```typescript
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
```
|
||||
|
||||
### 2. Next.js Router
|
||||
|
||||
```typescript
|
||||
const mockPush = jest.fn()
|
||||
const mockReplace = jest.fn()
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
replace: mockReplace,
|
||||
back: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
}),
|
||||
usePathname: () => '/current-path',
|
||||
useSearchParams: () => new URLSearchParams('?key=value'),
|
||||
}))
|
||||
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should navigate on click', () => {
|
||||
render(<Component />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(mockPush).toHaveBeenCalledWith('/expected-path')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Portal Components (with Shared State)
|
||||
|
||||
```typescript
|
||||
// ⚠️ Important: Use shared state for components that depend on each other
|
||||
let mockPortalOpenState = false
|
||||
|
||||
jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open, ...props }: any) => {
|
||||
mockPortalOpenState = open || false // Update shared state
|
||||
return <div data-testid="portal" data-open={open}>{children}</div>
|
||||
},
|
||||
PortalToFollowElemContent: ({ children }: any) => {
|
||||
// ✅ Matches actual: returns null when portal is closed
|
||||
if (!mockPortalOpenState) return null
|
||||
return <div data-testid="portal-content">{children}</div>
|
||||
},
|
||||
PortalToFollowElemTrigger: ({ children }: any) => (
|
||||
<div data-testid="portal-trigger">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPortalOpenState = false // ✅ Reset shared state
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4. API Service Mocks
|
||||
|
||||
```typescript
|
||||
import * as api from '@/service/api'
|
||||
|
||||
jest.mock('@/service/api')
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Setup default mock implementation
|
||||
mockedApi.fetchData.mockResolvedValue({ data: [] })
|
||||
})
|
||||
|
||||
it('should show data on success', async () => {
|
||||
mockedApi.fetchData.mockResolvedValue({ data: [{ id: 1 }] })
|
||||
|
||||
render(<Component />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error on failure', async () => {
|
||||
mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
render(<Component />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/error/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 5. HTTP Mocking with Nock
|
||||
|
||||
```typescript
|
||||
import nock from 'nock'
|
||||
|
||||
const GITHUB_HOST = 'https://api.github.com'
|
||||
const GITHUB_PATH = '/repos/owner/repo'
|
||||
|
||||
const mockGithubApi = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
||||
return nock(GITHUB_HOST)
|
||||
.get(GITHUB_PATH)
|
||||
.delay(delayMs)
|
||||
.reply(status, body)
|
||||
}
|
||||
|
||||
describe('GithubComponent', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it('should display repo info', async () => {
|
||||
mockGithubApi(200, { name: 'dify', stars: 1000 })
|
||||
|
||||
render(<GithubComponent />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('dify')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle API error', async () => {
|
||||
mockGithubApi(500, { message: 'Server error' })
|
||||
|
||||
render(<GithubComponent />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/error/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 6. Context Providers
|
||||
|
||||
```typescript
|
||||
import { ProviderContext } from '@/context/provider-context'
|
||||
import { createMockProviderContextValue, createMockPlan } from '@/__mocks__/provider-context'
|
||||
|
||||
describe('Component with Context', () => {
|
||||
it('should render for free plan', () => {
|
||||
const mockContext = createMockPlan('sandbox')
|
||||
|
||||
render(
|
||||
<ProviderContext.Provider value={mockContext}>
|
||||
<Component />
|
||||
</ProviderContext.Provider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Upgrade')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render for pro plan', () => {
|
||||
const mockContext = createMockPlan('professional')
|
||||
|
||||
render(
|
||||
<ProviderContext.Provider value={mockContext}>
|
||||
<Component />
|
||||
</ProviderContext.Provider>
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Upgrade')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 7. SWR / React Query
|
||||
|
||||
```typescript
|
||||
// SWR
|
||||
jest.mock('swr', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}))
|
||||
|
||||
import useSWR from 'swr'
|
||||
const mockedUseSWR = useSWR as jest.Mock
|
||||
|
||||
describe('Component with SWR', () => {
|
||||
it('should show loading state', () => {
|
||||
mockedUseSWR.mockReturnValue({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<Component />)
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// React Query
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const createTestQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithQueryClient = (ui: React.ReactElement) => {
|
||||
const queryClient = createTestQueryClient()
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Mock Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **Use real base components** - Import from `@/app/components/base/` directly
|
||||
1. **Use real project components** - Prefer importing over mocking
|
||||
1. **Reset mocks in `beforeEach`**, not `afterEach`
|
||||
1. **Match actual component behavior** in mocks (when mocking is necessary)
|
||||
1. **Use factory functions** for complex mock data
|
||||
1. **Import actual types** for type safety
|
||||
1. **Reset shared mock state** in `beforeEach`
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **Don't mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
||||
1. Don't mock components you can import directly
|
||||
1. Don't create overly simplified mocks that miss conditional logic
|
||||
1. Don't forget to clean up nock after each test
|
||||
1. Don't use `any` types in mocks without necessity
|
||||
|
||||
### Mock Decision Tree
|
||||
|
||||
```
|
||||
Need to use a component in test?
|
||||
│
|
||||
├─ Is it from @/app/components/base/*?
|
||||
│ └─ YES → Import real component, DO NOT mock
|
||||
│
|
||||
├─ Is it a project component?
|
||||
│ └─ YES → Prefer importing real component
|
||||
│ Only mock if setup is extremely complex
|
||||
│
|
||||
├─ Is it an API service (@/service/*)?
|
||||
│ └─ YES → Mock it
|
||||
│
|
||||
├─ Is it a third-party lib with side effects?
|
||||
│ └─ YES → Mock it (next/navigation, external SDKs)
|
||||
│
|
||||
└─ Is it i18n?
|
||||
└─ YES → Mock to return keys
|
||||
```
|
||||
|
||||
## Factory Function Pattern
|
||||
|
||||
```typescript
|
||||
// __mocks__/data-factories.ts
|
||||
import type { User, Project } from '@/types'
|
||||
|
||||
export const createMockUser = (overrides: Partial<User> = {}): User => ({
|
||||
id: 'user-1',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'member',
|
||||
createdAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
export const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
||||
id: 'project-1',
|
||||
name: 'Test Project',
|
||||
description: 'A test project',
|
||||
owner: createMockUser(),
|
||||
members: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Usage in tests
|
||||
it('should display project owner', () => {
|
||||
const project = createMockProject({
|
||||
owner: createMockUser({ name: 'John Doe' }),
|
||||
})
|
||||
|
||||
render(<ProjectCard project={project} />)
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
})
|
||||
```
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
# Testing Workflow Guide
|
||||
|
||||
This guide defines the workflow for generating tests, especially for complex components or directories with multiple files.
|
||||
|
||||
## Scope Clarification
|
||||
|
||||
This guide addresses **multi-file workflow** (how to process multiple test files). For coverage requirements within a single test file, see `web/testing/testing.md` § Coverage Goals.
|
||||
|
||||
| Scope | Rule |
|
||||
|-------|------|
|
||||
| **Single file** | Complete coverage in one generation (100% function, >95% branch) |
|
||||
| **Multi-file directory** | Process one file at a time, verify each before proceeding |
|
||||
|
||||
## ⚠️ Critical Rule: Incremental Approach for Multi-File Testing
|
||||
|
||||
When testing a **directory with multiple files**, **NEVER generate all test files at once.** Use an incremental, verify-as-you-go approach.
|
||||
|
||||
### Why Incremental?
|
||||
|
||||
| Batch Approach (❌) | Incremental Approach (✅) |
|
||||
|---------------------|---------------------------|
|
||||
| Generate 5+ tests at once | Generate 1 test at a time |
|
||||
| Run tests only at the end | Run test immediately after each file |
|
||||
| Multiple failures compound | Single point of failure, easy to debug |
|
||||
| Hard to identify root cause | Clear cause-effect relationship |
|
||||
| Mock issues affect many files | Mock issues caught early |
|
||||
| Messy git history | Clean, atomic commits possible |
|
||||
|
||||
## Single File Workflow
|
||||
|
||||
When testing a **single component, hook, or utility**:
|
||||
|
||||
```
|
||||
1. Read source code completely
|
||||
2. Run `pnpm analyze-component <path>` (if available)
|
||||
3. Check complexity score and features detected
|
||||
4. Write the test file
|
||||
5. Run test: `pnpm test -- <file>.spec.tsx`
|
||||
6. Fix any failures
|
||||
7. Verify coverage meets goals (100% function, >95% branch)
|
||||
```
|
||||
|
||||
## Directory/Multi-File Workflow (MUST FOLLOW)
|
||||
|
||||
When testing a **directory or multiple files**, follow this strict workflow:
|
||||
|
||||
### Step 1: Analyze and Plan
|
||||
|
||||
1. **List all files** that need tests in the directory
|
||||
1. **Categorize by complexity**:
|
||||
- 🟢 **Simple**: Utility functions, simple hooks, presentational components
|
||||
- 🟡 **Medium**: Components with state, effects, or event handlers
|
||||
- 🔴 **Complex**: Components with API calls, routing, or many dependencies
|
||||
1. **Order by dependency**: Test dependencies before dependents
|
||||
1. **Create a todo list** to track progress
|
||||
|
||||
### Step 2: Determine Processing Order
|
||||
|
||||
Process files in this recommended order:
|
||||
|
||||
```
|
||||
1. Utility functions (simplest, no React)
|
||||
2. Custom hooks (isolated logic)
|
||||
3. Simple presentational components (few/no props)
|
||||
4. Medium complexity components (state, effects)
|
||||
5. Complex components (API, routing, many deps)
|
||||
6. Container/index components (integration tests - last)
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- Simpler files help establish mock patterns
|
||||
- Hooks used by components should be tested first
|
||||
- Integration tests (index files) depend on child components working
|
||||
|
||||
### Step 3: Process Each File Incrementally
|
||||
|
||||
**For EACH file in the ordered list:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 1. Write test file │
|
||||
│ 2. Run: pnpm test -- <file>.spec.tsx │
|
||||
│ 3. If FAIL → Fix immediately, re-run │
|
||||
│ 4. If PASS → Mark complete in todo list │
|
||||
│ 5. ONLY THEN proceed to next file │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**DO NOT proceed to the next file until the current one passes.**
|
||||
|
||||
### Step 4: Final Verification
|
||||
|
||||
After all individual tests pass:
|
||||
|
||||
```bash
|
||||
# Run all tests in the directory together
|
||||
pnpm test -- path/to/directory/
|
||||
|
||||
# Check coverage
|
||||
pnpm test -- --coverage path/to/directory/
|
||||
```
|
||||
|
||||
## Component Complexity Guidelines
|
||||
|
||||
Use `pnpm analyze-component <path>` to assess complexity before testing.
|
||||
|
||||
### 🔴 Very Complex Components (Complexity > 50)
|
||||
|
||||
**Consider refactoring BEFORE testing:**
|
||||
|
||||
- Break component into smaller, testable pieces
|
||||
- Extract complex logic into custom hooks
|
||||
- Separate container and presentational layers
|
||||
|
||||
**If testing as-is:**
|
||||
|
||||
- Use integration tests for complex workflows
|
||||
- Use `test.each()` for data-driven testing
|
||||
- Multiple `describe` blocks for organization
|
||||
- Consider testing major sections separately
|
||||
|
||||
### 🟡 Medium Complexity (Complexity 30-50)
|
||||
|
||||
- Group related tests in `describe` blocks
|
||||
- Test integration scenarios between internal parts
|
||||
- Focus on state transitions and side effects
|
||||
- Use helper functions to reduce test complexity
|
||||
|
||||
### 🟢 Simple Components (Complexity < 30)
|
||||
|
||||
- Standard test structure
|
||||
- Focus on props, rendering, and edge cases
|
||||
- Usually straightforward to test
|
||||
|
||||
### 📏 Large Files (500+ lines)
|
||||
|
||||
Regardless of complexity score:
|
||||
|
||||
- **Strongly consider refactoring** before testing
|
||||
- If testing as-is, test major sections separately
|
||||
- Create helper functions for test setup
|
||||
- May need multiple test files
|
||||
|
||||
## Todo List Format
|
||||
|
||||
When testing multiple files, use a todo list like this:
|
||||
|
||||
```
|
||||
Testing: path/to/directory/
|
||||
|
||||
Ordered by complexity (simple → complex):
|
||||
|
||||
☐ utils/helper.ts [utility, simple]
|
||||
☐ hooks/use-custom-hook.ts [hook, simple]
|
||||
☐ empty-state.tsx [component, simple]
|
||||
☐ item-card.tsx [component, medium]
|
||||
☐ list.tsx [component, complex]
|
||||
☐ index.tsx [integration]
|
||||
|
||||
Progress: 0/6 complete
|
||||
```
|
||||
|
||||
Update status as you complete each:
|
||||
|
||||
- ☐ → ⏳ (in progress)
|
||||
- ⏳ → ✅ (complete and verified)
|
||||
- ⏳ → ❌ (blocked, needs attention)
|
||||
|
||||
## When to Stop and Verify
|
||||
|
||||
**Always run tests after:**
|
||||
|
||||
- Completing a test file
|
||||
- Making changes to fix a failure
|
||||
- Modifying shared mocks
|
||||
- Updating test utilities or helpers
|
||||
|
||||
**Signs you should pause:**
|
||||
|
||||
- More than 2 consecutive test failures
|
||||
- Mock-related errors appearing
|
||||
- Unclear why a test is failing
|
||||
- Test passing but coverage unexpectedly low
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
### ❌ Don't: Generate Everything First
|
||||
|
||||
```
|
||||
# BAD: Writing all files then testing
|
||||
Write component-a.spec.tsx
|
||||
Write component-b.spec.tsx
|
||||
Write component-c.spec.tsx
|
||||
Write component-d.spec.tsx
|
||||
Run pnpm test ← Multiple failures, hard to debug
|
||||
```
|
||||
|
||||
### ✅ Do: Verify Each Step
|
||||
|
||||
```
|
||||
# GOOD: Incremental with verification
|
||||
Write component-a.spec.tsx
|
||||
Run pnpm test -- component-a.spec.tsx ✅
|
||||
Write component-b.spec.tsx
|
||||
Run pnpm test -- component-b.spec.tsx ✅
|
||||
...continue...
|
||||
```
|
||||
|
||||
### ❌ Don't: Skip Verification for "Simple" Components
|
||||
|
||||
Even simple components can have:
|
||||
|
||||
- Import errors
|
||||
- Missing mock setup
|
||||
- Incorrect assumptions about props
|
||||
|
||||
**Always verify, regardless of perceived simplicity.**
|
||||
|
||||
### ❌ Don't: Continue When Tests Fail
|
||||
|
||||
Failing tests compound:
|
||||
|
||||
- A mock issue in file A affects files B, C, D
|
||||
- Fixing A later requires revisiting all dependent tests
|
||||
- Time wasted on debugging cascading failures
|
||||
|
||||
**Fix failures immediately before proceeding.**
|
||||
|
||||
## Integration with Claude's Todo Feature
|
||||
|
||||
When using Claude for multi-file testing:
|
||||
|
||||
1. **Ask Claude to create a todo list** before starting
|
||||
1. **Request one file at a time** or ensure Claude processes incrementally
|
||||
1. **Verify each test passes** before asking for the next
|
||||
1. **Mark todos complete** as you progress
|
||||
|
||||
Example prompt:
|
||||
|
||||
```
|
||||
Test all components in `path/to/directory/`.
|
||||
First, analyze the directory and create a todo list ordered by complexity.
|
||||
Then, process ONE file at a time, waiting for my confirmation that tests pass
|
||||
before proceeding to the next.
|
||||
```
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before starting multi-file testing:
|
||||
|
||||
- [ ] Listed all files needing tests
|
||||
- [ ] Ordered by complexity (simple → complex)
|
||||
- [ ] Created todo list for tracking
|
||||
- [ ] Understand dependencies between files
|
||||
|
||||
During testing:
|
||||
|
||||
- [ ] Processing ONE file at a time
|
||||
- [ ] Running tests after EACH file
|
||||
- [ ] Fixing failures BEFORE proceeding
|
||||
- [ ] Updating todo list progress
|
||||
|
||||
After completion:
|
||||
|
||||
- [ ] All individual tests pass
|
||||
- [ ] Full directory test run passes
|
||||
- [ ] Coverage goals met
|
||||
- [ ] Todo list shows all complete
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* Test Template for React Components
|
||||
*
|
||||
* WHY THIS STRUCTURE?
|
||||
* - Organized sections make tests easy to navigate and maintain
|
||||
* - Mocks at top ensure consistent test isolation
|
||||
* - Factory functions reduce duplication and improve readability
|
||||
* - describe blocks group related scenarios for better debugging
|
||||
*
|
||||
* INSTRUCTIONS:
|
||||
* 1. Replace `ComponentName` with your component name
|
||||
* 2. Update import path
|
||||
* 3. Add/remove test sections based on component features (use analyze-component)
|
||||
* 4. Follow AAA pattern: Arrange → Act → Assert
|
||||
*
|
||||
* RUN FIRST: pnpm analyze-component <path> to identify required test scenarios
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
// import ComponentName from './index'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
// WHY: Mocks must be hoisted to top of file (Jest requirement).
|
||||
// They run BEFORE imports, so keep them before component imports.
|
||||
|
||||
// i18n (always required in Dify)
|
||||
// WHY: Returns key instead of translation so tests don't depend on i18n files
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Router (if component uses useRouter, usePathname, useSearchParams)
|
||||
// WHY: Isolates tests from Next.js routing, enables testing navigation behavior
|
||||
// const mockPush = jest.fn()
|
||||
// jest.mock('next/navigation', () => ({
|
||||
// useRouter: () => ({ push: mockPush }),
|
||||
// usePathname: () => '/test-path',
|
||||
// }))
|
||||
|
||||
// API services (if component fetches data)
|
||||
// WHY: Prevents real network calls, enables testing all states (loading/success/error)
|
||||
// jest.mock('@/service/api')
|
||||
// import * as api from '@/service/api'
|
||||
// const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
// Shared mock state (for portal/dropdown components)
|
||||
// WHY: Portal components like PortalToFollowElem need shared state between
|
||||
// parent and child mocks to correctly simulate open/close behavior
|
||||
// let mockOpenState = false
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
// WHY FACTORIES?
|
||||
// - Avoid hard-coded test data scattered across tests
|
||||
// - Easy to create variations with overrides
|
||||
// - Type-safe when using actual types from source
|
||||
// - Single source of truth for default test values
|
||||
|
||||
// const createMockProps = (overrides = {}) => ({
|
||||
// // Default props that make component render successfully
|
||||
// ...overrides,
|
||||
// })
|
||||
|
||||
// const createMockItem = (overrides = {}) => ({
|
||||
// id: 'item-1',
|
||||
// name: 'Test Item',
|
||||
// ...overrides,
|
||||
// })
|
||||
|
||||
// ============================================================================
|
||||
// Test Helpers
|
||||
// ============================================================================
|
||||
|
||||
// const renderComponent = (props = {}) => {
|
||||
// return render(<ComponentName {...createMockProps(props)} />)
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('ComponentName', () => {
|
||||
// WHY beforeEach with clearAllMocks?
|
||||
// - Ensures each test starts with clean slate
|
||||
// - Prevents mock call history from leaking between tests
|
||||
// - MUST be beforeEach (not afterEach) to reset BEFORE assertions like toHaveBeenCalledTimes
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Reset shared mock state if used (CRITICAL for portal/dropdown tests)
|
||||
// mockOpenState = false
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests (REQUIRED - Every component MUST have these)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Catches import errors, missing providers, and basic render issues
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange - Setup data and mocks
|
||||
// const props = createMockProps()
|
||||
|
||||
// Act - Render the component
|
||||
// render(<ComponentName {...props} />)
|
||||
|
||||
// Assert - Verify expected output
|
||||
// Prefer getByRole for accessibility; it's what users "see"
|
||||
// expect(screen.getByRole('...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with default props', () => {
|
||||
// WHY: Verifies component works without optional props
|
||||
// render(<ComponentName />)
|
||||
// expect(screen.getByText('...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Tests (REQUIRED - Every component MUST test prop behavior)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Props are the component's API contract. Test them thoroughly.
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
// WHY: Common pattern in Dify - components should merge custom classes
|
||||
// render(<ComponentName className="custom-class" />)
|
||||
// expect(screen.getByTestId('component')).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should use default values for optional props', () => {
|
||||
// WHY: Verifies TypeScript defaults work at runtime
|
||||
// render(<ComponentName />)
|
||||
// expect(screen.getByRole('...')).toHaveAttribute('...', 'default-value')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions (if component has event handlers - on*, handle*)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Event handlers are core functionality. Test from user's perspective.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onClick when clicked', async () => {
|
||||
// WHY userEvent over fireEvent?
|
||||
// - userEvent simulates real user behavior (focus, hover, then click)
|
||||
// - fireEvent is lower-level, doesn't trigger all browser events
|
||||
// const user = userEvent.setup()
|
||||
// const handleClick = jest.fn()
|
||||
// render(<ComponentName onClick={handleClick} />)
|
||||
//
|
||||
// await user.click(screen.getByRole('button'))
|
||||
//
|
||||
// expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onChange when value changes', async () => {
|
||||
// const user = userEvent.setup()
|
||||
// const handleChange = jest.fn()
|
||||
// render(<ComponentName onChange={handleChange} />)
|
||||
//
|
||||
// await user.type(screen.getByRole('textbox'), 'new value')
|
||||
//
|
||||
// expect(handleChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State Management (if component uses useState/useReducer)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Test state through observable UI changes, not internal state values
|
||||
describe('State Management', () => {
|
||||
it('should update state on interaction', async () => {
|
||||
// WHY test via UI, not state?
|
||||
// - State is implementation detail; UI is what users see
|
||||
// - If UI works correctly, state must be correct
|
||||
// const user = userEvent.setup()
|
||||
// render(<ComponentName />)
|
||||
//
|
||||
// // Initial state - verify what user sees
|
||||
// expect(screen.getByText('Initial')).toBeInTheDocument()
|
||||
//
|
||||
// // Trigger state change via user action
|
||||
// await user.click(screen.getByRole('button'))
|
||||
//
|
||||
// // New state - verify UI updated
|
||||
// expect(screen.getByText('Updated')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Async Operations (if component fetches data - useSWR, useQuery, fetch)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Async operations have 3 states users experience: loading, success, error
|
||||
describe('Async Operations', () => {
|
||||
it('should show loading state', () => {
|
||||
// WHY never-resolving promise?
|
||||
// - Keeps component in loading state for assertion
|
||||
// - Alternative: use fake timers
|
||||
// mockedApi.fetchData.mockImplementation(() => new Promise(() => {}))
|
||||
// render(<ComponentName />)
|
||||
//
|
||||
// expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show data on success', async () => {
|
||||
// WHY waitFor?
|
||||
// - Component updates asynchronously after fetch resolves
|
||||
// - waitFor retries assertion until it passes or times out
|
||||
// mockedApi.fetchData.mockResolvedValue({ items: ['Item 1'] })
|
||||
// render(<ComponentName />)
|
||||
//
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByText('Item 1')).toBeInTheDocument()
|
||||
// })
|
||||
})
|
||||
|
||||
it('should show error on failure', async () => {
|
||||
// mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
|
||||
// render(<ComponentName />)
|
||||
//
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByText(/error/i)).toBeInTheDocument()
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases (REQUIRED - Every component MUST handle edge cases)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Real-world data is messy. Components must handle:
|
||||
// - Null/undefined from API failures or optional fields
|
||||
// - Empty arrays/strings from user clearing data
|
||||
// - Boundary values (0, MAX_INT, special characters)
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle null value', () => {
|
||||
// WHY test null specifically?
|
||||
// - API might return null for missing data
|
||||
// - Prevents "Cannot read property of null" in production
|
||||
// render(<ComponentName value={null} />)
|
||||
// expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined value', () => {
|
||||
// WHY test undefined separately from null?
|
||||
// - TypeScript treats them differently
|
||||
// - Optional props are undefined, not null
|
||||
// render(<ComponentName value={undefined} />)
|
||||
// expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// WHY: Empty state often needs special UI (e.g., "No items yet")
|
||||
// render(<ComponentName items={[]} />)
|
||||
// expect(screen.getByText(/empty/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
// WHY: Empty strings are truthy in JS but visually empty
|
||||
// render(<ComponentName text="" />)
|
||||
// expect(screen.getByText(/placeholder/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility (optional but recommended for Dify's enterprise users)
|
||||
// --------------------------------------------------------------------------
|
||||
// WHY: Dify has enterprise customers who may require accessibility compliance
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible name', () => {
|
||||
// WHY getByRole with name?
|
||||
// - Tests that screen readers can identify the element
|
||||
// - Enforces proper labeling practices
|
||||
// render(<ComponentName label="Test Label" />)
|
||||
// expect(screen.getByRole('button', { name: /test label/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should support keyboard navigation', async () => {
|
||||
// WHY: Some users can't use a mouse
|
||||
// const user = userEvent.setup()
|
||||
// render(<ComponentName />)
|
||||
//
|
||||
// await user.tab()
|
||||
// expect(screen.getByRole('button')).toHaveFocus()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* Test Template for Custom Hooks
|
||||
*
|
||||
* Instructions:
|
||||
* 1. Replace `useHookName` with your hook name
|
||||
* 2. Update import path
|
||||
* 3. Add/remove test sections based on hook features
|
||||
*/
|
||||
|
||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||
// import { useHookName } from './use-hook-name'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// API services (if hook fetches data)
|
||||
// jest.mock('@/service/api')
|
||||
// import * as api from '@/service/api'
|
||||
// const mockedApi = api as jest.Mocked<typeof api>
|
||||
|
||||
// ============================================================================
|
||||
// Test Helpers
|
||||
// ============================================================================
|
||||
|
||||
// Wrapper for hooks that need context
|
||||
// const createWrapper = (contextValue = {}) => {
|
||||
// return ({ children }: { children: React.ReactNode }) => (
|
||||
// <SomeContext.Provider value={contextValue}>
|
||||
// {children}
|
||||
// </SomeContext.Provider>
|
||||
// )
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useHookName', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Initial State
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Initial State', () => {
|
||||
it('should return initial state', () => {
|
||||
// const { result } = renderHook(() => useHookName())
|
||||
//
|
||||
// expect(result.current.value).toBe(initialValue)
|
||||
// expect(result.current.isLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('should accept initial value from props', () => {
|
||||
// const { result } = renderHook(() => useHookName({ initialValue: 'custom' }))
|
||||
//
|
||||
// expect(result.current.value).toBe('custom')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State Updates
|
||||
// --------------------------------------------------------------------------
|
||||
describe('State Updates', () => {
|
||||
it('should update value when setValue is called', () => {
|
||||
// const { result } = renderHook(() => useHookName())
|
||||
//
|
||||
// act(() => {
|
||||
// result.current.setValue('new value')
|
||||
// })
|
||||
//
|
||||
// expect(result.current.value).toBe('new value')
|
||||
})
|
||||
|
||||
it('should reset to initial value', () => {
|
||||
// const { result } = renderHook(() => useHookName({ initialValue: 'initial' }))
|
||||
//
|
||||
// act(() => {
|
||||
// result.current.setValue('changed')
|
||||
// })
|
||||
// expect(result.current.value).toBe('changed')
|
||||
//
|
||||
// act(() => {
|
||||
// result.current.reset()
|
||||
// })
|
||||
// expect(result.current.value).toBe('initial')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Async Operations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Async Operations', () => {
|
||||
it('should fetch data on mount', async () => {
|
||||
// mockedApi.fetchData.mockResolvedValue({ data: 'test' })
|
||||
//
|
||||
// const { result } = renderHook(() => useHookName())
|
||||
//
|
||||
// // Initially loading
|
||||
// expect(result.current.isLoading).toBe(true)
|
||||
//
|
||||
// // Wait for data
|
||||
// await waitFor(() => {
|
||||
// expect(result.current.isLoading).toBe(false)
|
||||
// })
|
||||
//
|
||||
// expect(result.current.data).toEqual({ data: 'test' })
|
||||
})
|
||||
|
||||
it('should handle fetch error', async () => {
|
||||
// mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
|
||||
//
|
||||
// const { result } = renderHook(() => useHookName())
|
||||
//
|
||||
// await waitFor(() => {
|
||||
// expect(result.current.error).toBeTruthy()
|
||||
// })
|
||||
//
|
||||
// expect(result.current.error?.message).toBe('Network error')
|
||||
})
|
||||
|
||||
it('should refetch when dependency changes', async () => {
|
||||
// mockedApi.fetchData.mockResolvedValue({ data: 'test' })
|
||||
//
|
||||
// const { result, rerender } = renderHook(
|
||||
// ({ id }) => useHookName(id),
|
||||
// { initialProps: { id: '1' } }
|
||||
// )
|
||||
//
|
||||
// await waitFor(() => {
|
||||
// expect(mockedApi.fetchData).toHaveBeenCalledWith('1')
|
||||
// })
|
||||
//
|
||||
// rerender({ id: '2' })
|
||||
//
|
||||
// await waitFor(() => {
|
||||
// expect(mockedApi.fetchData).toHaveBeenCalledWith('2')
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Side Effects
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Side Effects', () => {
|
||||
it('should call callback when value changes', () => {
|
||||
// const callback = jest.fn()
|
||||
// const { result } = renderHook(() => useHookName({ onChange: callback }))
|
||||
//
|
||||
// act(() => {
|
||||
// result.current.setValue('new value')
|
||||
// })
|
||||
//
|
||||
// expect(callback).toHaveBeenCalledWith('new value')
|
||||
})
|
||||
|
||||
it('should cleanup on unmount', () => {
|
||||
// const cleanup = jest.fn()
|
||||
// jest.spyOn(window, 'addEventListener')
|
||||
// jest.spyOn(window, 'removeEventListener')
|
||||
//
|
||||
// const { unmount } = renderHook(() => useHookName())
|
||||
//
|
||||
// expect(window.addEventListener).toHaveBeenCalled()
|
||||
//
|
||||
// unmount()
|
||||
//
|
||||
// expect(window.removeEventListener).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle null input', () => {
|
||||
// const { result } = renderHook(() => useHookName(null))
|
||||
//
|
||||
// expect(result.current.value).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle rapid updates', () => {
|
||||
// const { result } = renderHook(() => useHookName())
|
||||
//
|
||||
// act(() => {
|
||||
// result.current.setValue('1')
|
||||
// result.current.setValue('2')
|
||||
// result.current.setValue('3')
|
||||
// })
|
||||
//
|
||||
// expect(result.current.value).toBe('3')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// With Context (if hook uses context)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('With Context', () => {
|
||||
it('should use context value', () => {
|
||||
// const wrapper = createWrapper({ someValue: 'context-value' })
|
||||
// const { result } = renderHook(() => useHookName(), { wrapper })
|
||||
//
|
||||
// expect(result.current.contextValue).toBe('context-value')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Test Template for Utility Functions
|
||||
*
|
||||
* Instructions:
|
||||
* 1. Replace `utilityFunction` with your function name
|
||||
* 2. Update import path
|
||||
* 3. Use test.each for data-driven tests
|
||||
*/
|
||||
|
||||
// import { utilityFunction } from './utility'
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('utilityFunction', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
// Basic Functionality
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Basic Functionality', () => {
|
||||
it('should return expected result for valid input', () => {
|
||||
// expect(utilityFunction('input')).toBe('expected-output')
|
||||
})
|
||||
|
||||
it('should handle multiple arguments', () => {
|
||||
// expect(utilityFunction('a', 'b', 'c')).toBe('abc')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Data-Driven Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Input/Output Mapping', () => {
|
||||
test.each([
|
||||
// [input, expected]
|
||||
['input1', 'output1'],
|
||||
['input2', 'output2'],
|
||||
['input3', 'output3'],
|
||||
])('should return %s for input %s', (input, expected) => {
|
||||
// expect(utilityFunction(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string', () => {
|
||||
// expect(utilityFunction('')).toBe('')
|
||||
})
|
||||
|
||||
it('should handle null', () => {
|
||||
// expect(utilityFunction(null)).toBe(null)
|
||||
// or
|
||||
// expect(() => utilityFunction(null)).toThrow()
|
||||
})
|
||||
|
||||
it('should handle undefined', () => {
|
||||
// expect(utilityFunction(undefined)).toBe(undefined)
|
||||
// or
|
||||
// expect(() => utilityFunction(undefined)).toThrow()
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// expect(utilityFunction([])).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle empty object', () => {
|
||||
// expect(utilityFunction({})).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Boundary Conditions
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Boundary Conditions', () => {
|
||||
it('should handle minimum value', () => {
|
||||
// expect(utilityFunction(0)).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle maximum value', () => {
|
||||
// expect(utilityFunction(Number.MAX_SAFE_INTEGER)).toBe(...)
|
||||
})
|
||||
|
||||
it('should handle negative numbers', () => {
|
||||
// expect(utilityFunction(-1)).toBe(...)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Type Coercion (if applicable)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Type Handling', () => {
|
||||
it('should handle numeric string', () => {
|
||||
// expect(utilityFunction('123')).toBe(123)
|
||||
})
|
||||
|
||||
it('should handle boolean', () => {
|
||||
// expect(utilityFunction(true)).toBe(...)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Error Cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Error Handling', () => {
|
||||
it('should throw for invalid input', () => {
|
||||
// expect(() => utilityFunction('invalid')).toThrow('Error message')
|
||||
})
|
||||
|
||||
it('should throw with specific error type', () => {
|
||||
// expect(() => utilityFunction('invalid')).toThrow(ValidationError)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Complex Objects (if applicable)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Object Handling', () => {
|
||||
it('should preserve object structure', () => {
|
||||
// const input = { a: 1, b: 2 }
|
||||
// expect(utilityFunction(input)).toEqual({ a: 1, b: 2 })
|
||||
})
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
// const input = { nested: { deep: 'value' } }
|
||||
// expect(utilityFunction(input)).toEqual({ nested: { deep: 'transformed' } })
|
||||
})
|
||||
|
||||
it('should not mutate input', () => {
|
||||
// const input = { a: 1 }
|
||||
// const inputCopy = { ...input }
|
||||
// utilityFunction(input)
|
||||
// expect(input).toEqual(inputCopy)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Array Handling (if applicable)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Array Handling', () => {
|
||||
it('should process all elements', () => {
|
||||
// expect(utilityFunction([1, 2, 3])).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
it('should handle single element array', () => {
|
||||
// expect(utilityFunction([1])).toEqual([2])
|
||||
})
|
||||
|
||||
it('should preserve order', () => {
|
||||
// expect(utilityFunction(['c', 'a', 'b'])).toEqual(['c', 'a', 'b'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# Copilot Instructions
|
||||
|
||||
GitHub Copilot must follow the unified frontend testing requirements documented in `web/testing/testing.md`.
|
||||
|
||||
Key reminders:
|
||||
|
||||
- Generate tests using the mandated tech stack, naming, and code style (AAA pattern, `fireEvent`, descriptive test names, cleans up mocks).
|
||||
- Cover rendering, prop combinations, and edge cases by default; extend coverage for hooks, routing, async flows, and domain-specific components when applicable.
|
||||
- Target >95% line and branch coverage and 100% function/statement coverage.
|
||||
- Apply the project's mocking conventions for i18n, toast notifications, and Next.js utilities.
|
||||
|
||||
Any suggestions from Copilot that conflict with `web/testing/testing.md` should be revised before acceptance.
|
||||
|
|
@ -13,11 +13,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Use uv to ensure we have the same ruff version in CI and locally.
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- run: |
|
||||
cd api
|
||||
uv sync --dev
|
||||
|
|
@ -35,10 +36,11 @@ jobs:
|
|||
|
||||
- name: ast-grep
|
||||
run: |
|
||||
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||
uvx --from ast-grep-cli sg --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||
uvx --from ast-grep-cli sg -p '$A = db.Column($$$B)' -r '$A = mapped_column($$$B)' -l py --update-all
|
||||
uvx --from ast-grep-cli sg -p '$A : $T = db.Column($$$B)' -r '$A : $T = mapped_column($$$B)' -l py --update-all
|
||||
# ast-grep exits 1 if no matches are found; allow idempotent runs.
|
||||
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
||||
uvx --from ast-grep-cli ast-grep --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
||||
uvx --from ast-grep-cli ast-grep -p '$A = db.Column($$$B)' -r '$A = mapped_column($$$B)' -l py --update-all || true
|
||||
uvx --from ast-grep-cli ast-grep -p '$A : $T = db.Column($$$B)' -r '$A : $T = mapped_column($$$B)' -l py --update-all || true
|
||||
# Convert Optional[T] to T | None (ignoring quoted types)
|
||||
cat > /tmp/optional-rule.yml << 'EOF'
|
||||
id: convert-optional-to-union
|
||||
|
|
@ -56,14 +58,15 @@ jobs:
|
|||
pattern: $T
|
||||
fix: $T | None
|
||||
EOF
|
||||
uvx --from ast-grep-cli sg scan --inline-rules "$(cat /tmp/optional-rule.yml)" --update-all
|
||||
uvx --from ast-grep-cli ast-grep scan . --inline-rules "$(cat /tmp/optional-rule.yml)" --update-all
|
||||
# Fix forward references that were incorrectly converted (Python doesn't support "Type" | None syntax)
|
||||
find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \;
|
||||
find . -name "*.py.bak" -type f -delete
|
||||
|
||||
# mdformat breaks YAML front matter in markdown files. Add --exclude for directories containing YAML front matter.
|
||||
- name: mdformat
|
||||
run: |
|
||||
uvx mdformat .
|
||||
uvx --python 3.13 mdformat . --exclude ".claude/skills/**"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
|
@ -84,7 +87,6 @@ jobs:
|
|||
|
||||
- name: oxlint
|
||||
working-directory: ./web
|
||||
run: |
|
||||
pnpx oxlint --fix
|
||||
run: pnpm exec oxlint --config .oxlintrc.json --fix .
|
||||
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ docker/volumes/matrixone/*
|
|||
docker/volumes/mysql/*
|
||||
docker/volumes/seekdb/*
|
||||
!docker/volumes/oceanbase/init.d
|
||||
docker/volumes/iris/*
|
||||
|
||||
docker/nginx/conf.d/default.conf
|
||||
docker/nginx/ssl/*
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
# Windsurf Testing Rules
|
||||
|
||||
- Use `web/testing/testing.md` as the single source of truth for frontend automated testing.
|
||||
- Honor every requirement in that document when generating or accepting tests.
|
||||
- When proposing or saving tests, re-read that document and follow every requirement.
|
||||
|
|
@ -26,6 +26,7 @@ from .vdb.clickzetta_config import ClickzettaConfig
|
|||
from .vdb.couchbase_config import CouchbaseConfig
|
||||
from .vdb.elasticsearch_config import ElasticsearchConfig
|
||||
from .vdb.huawei_cloud_config import HuaweiCloudConfig
|
||||
from .vdb.iris_config import IrisVectorConfig
|
||||
from .vdb.lindorm_config import LindormConfig
|
||||
from .vdb.matrixone_config import MatrixoneConfig
|
||||
from .vdb.milvus_config import MilvusConfig
|
||||
|
|
@ -336,6 +337,7 @@ class MiddlewareConfig(
|
|||
ChromaConfig,
|
||||
ClickzettaConfig,
|
||||
HuaweiCloudConfig,
|
||||
IrisVectorConfig,
|
||||
MilvusConfig,
|
||||
AlibabaCloudMySQLConfig,
|
||||
MyScaleConfig,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
"""Configuration for InterSystems IRIS vector database."""
|
||||
|
||||
from pydantic import Field, PositiveInt, model_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class IrisVectorConfig(BaseSettings):
|
||||
"""Configuration settings for IRIS vector database connection and pooling."""
|
||||
|
||||
IRIS_HOST: str | None = Field(
|
||||
description="Hostname or IP address of the IRIS server.",
|
||||
default="localhost",
|
||||
)
|
||||
|
||||
IRIS_SUPER_SERVER_PORT: PositiveInt | None = Field(
|
||||
description="Port number for IRIS connection.",
|
||||
default=1972,
|
||||
)
|
||||
|
||||
IRIS_USER: str | None = Field(
|
||||
description="Username for IRIS authentication.",
|
||||
default="_SYSTEM",
|
||||
)
|
||||
|
||||
IRIS_PASSWORD: str | None = Field(
|
||||
description="Password for IRIS authentication.",
|
||||
default="Dify@1234",
|
||||
)
|
||||
|
||||
IRIS_SCHEMA: str | None = Field(
|
||||
description="Schema name for IRIS tables.",
|
||||
default="dify",
|
||||
)
|
||||
|
||||
IRIS_DATABASE: str | None = Field(
|
||||
description="Database namespace for IRIS connection.",
|
||||
default="USER",
|
||||
)
|
||||
|
||||
IRIS_CONNECTION_URL: str | None = Field(
|
||||
description="Full connection URL for IRIS (overrides individual fields if provided).",
|
||||
default=None,
|
||||
)
|
||||
|
||||
IRIS_MIN_CONNECTION: PositiveInt = Field(
|
||||
description="Minimum number of connections in the pool.",
|
||||
default=1,
|
||||
)
|
||||
|
||||
IRIS_MAX_CONNECTION: PositiveInt = Field(
|
||||
description="Maximum number of connections in the pool.",
|
||||
default=3,
|
||||
)
|
||||
|
||||
IRIS_TEXT_INDEX: bool = Field(
|
||||
description="Enable full-text search index using %iFind.Index.Basic.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
IRIS_TEXT_INDEX_LANGUAGE: str = Field(
|
||||
description="Language for full-text search index (e.g., 'en', 'ja', 'zh', 'de').",
|
||||
default="en",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_config(cls, values: dict) -> dict:
|
||||
"""Validate IRIS configuration values.
|
||||
|
||||
Args:
|
||||
values: Configuration dictionary
|
||||
|
||||
Returns:
|
||||
Validated configuration dictionary
|
||||
|
||||
Raises:
|
||||
ValueError: If required fields are missing or pool settings are invalid
|
||||
"""
|
||||
# Only validate required fields if IRIS is being used as the vector store
|
||||
# This allows the config to be loaded even when IRIS is not in use
|
||||
|
||||
# vector_store = os.environ.get("VECTOR_STORE", "")
|
||||
# We rely on Pydantic defaults for required fields if they are missing from env.
|
||||
# Strict existence check is removed to allow defaults to work.
|
||||
|
||||
min_conn = values.get("IRIS_MIN_CONNECTION", 1)
|
||||
max_conn = values.get("IRIS_MAX_CONNECTION", 3)
|
||||
if min_conn > max_conn:
|
||||
raise ValueError("IRIS_MIN_CONNECTION must be less than or equal to IRIS_MAX_CONNECTION")
|
||||
|
||||
return values
|
||||
|
|
@ -20,6 +20,7 @@ language_timezone_mapping = {
|
|||
"sl-SI": "Europe/Ljubljana",
|
||||
"th-TH": "Asia/Bangkok",
|
||||
"id-ID": "Asia/Jakarta",
|
||||
"ar-TN": "Africa/Tunis",
|
||||
}
|
||||
|
||||
languages = list(language_timezone_mapping.keys())
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ def _get_retrieval_methods_by_vector_type(vector_type: str | None, is_mock: bool
|
|||
VectorType.CLICKZETTA,
|
||||
VectorType.BAIDU,
|
||||
VectorType.ALIBABACLOUD_MYSQL,
|
||||
VectorType.IRIS,
|
||||
}
|
||||
|
||||
semantic_methods = {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ class PluginDebuggingKeyApi(Resource):
|
|||
|
||||
|
||||
class ParserList(BaseModel):
|
||||
page: int = Field(default=1)
|
||||
page_size: int = Field(default=256)
|
||||
page: int = Field(default=1, ge=1, description="Page number")
|
||||
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
|
||||
|
||||
|
||||
reg(ParserList)
|
||||
|
|
@ -106,8 +106,8 @@ class ParserPluginIdentifierQuery(BaseModel):
|
|||
|
||||
|
||||
class ParserTasks(BaseModel):
|
||||
page: int
|
||||
page_size: int
|
||||
page: int = Field(default=1, ge=1, description="Page number")
|
||||
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
|
||||
|
||||
|
||||
class ParserMarketplaceUpgrade(BaseModel):
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def trigger_endpoint(endpoint_id: str):
|
|||
if response:
|
||||
break
|
||||
if not response:
|
||||
logger.error("Endpoint not found for {endpoint_id}")
|
||||
logger.info("Endpoint not found for %s", endpoint_id)
|
||||
return jsonify({"error": "Endpoint not found"}), 404
|
||||
return response
|
||||
except ValueError as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,407 @@
|
|||
"""InterSystems IRIS vector database implementation for Dify.
|
||||
|
||||
This module provides vector storage and retrieval using IRIS native VECTOR type
|
||||
with HNSW indexing for efficient similarity search.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from configs import dify_config
|
||||
from configs.middleware.vdb.iris_config import IrisVectorConfig
|
||||
from core.rag.datasource.vdb.vector_base import BaseVector
|
||||
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
|
||||
from core.rag.datasource.vdb.vector_type import VectorType
|
||||
from core.rag.embedding.embedding_base import Embeddings
|
||||
from core.rag.models.document import Document
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import Dataset
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import iris
|
||||
else:
|
||||
try:
|
||||
import iris
|
||||
except ImportError:
|
||||
iris = None # type: ignore[assignment]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Singleton connection pool to minimize IRIS license usage
|
||||
_pool_lock = threading.Lock()
|
||||
_pool_instance: IrisConnectionPool | None = None
|
||||
|
||||
|
||||
def get_iris_pool(config: IrisVectorConfig) -> IrisConnectionPool:
|
||||
"""Get or create the global IRIS connection pool (singleton pattern)."""
|
||||
global _pool_instance # pylint: disable=global-statement
|
||||
with _pool_lock:
|
||||
if _pool_instance is None:
|
||||
logger.info("Initializing IRIS connection pool")
|
||||
_pool_instance = IrisConnectionPool(config)
|
||||
return _pool_instance
|
||||
|
||||
|
||||
class IrisConnectionPool:
|
||||
"""Thread-safe connection pool for IRIS database."""
|
||||
|
||||
def __init__(self, config: IrisVectorConfig) -> None:
|
||||
self.config = config
|
||||
self._pool: list[Any] = []
|
||||
self._lock = threading.Lock()
|
||||
self._min_size = config.IRIS_MIN_CONNECTION
|
||||
self._max_size = config.IRIS_MAX_CONNECTION
|
||||
self._in_use = 0
|
||||
self._schemas_initialized: set[str] = set() # Cache for initialized schemas
|
||||
self._initialize_pool()
|
||||
|
||||
def _initialize_pool(self) -> None:
|
||||
for _ in range(self._min_size):
|
||||
self._pool.append(self._create_connection())
|
||||
|
||||
def _create_connection(self) -> Any:
|
||||
return iris.connect(
|
||||
hostname=self.config.IRIS_HOST,
|
||||
port=self.config.IRIS_SUPER_SERVER_PORT,
|
||||
namespace=self.config.IRIS_DATABASE,
|
||||
username=self.config.IRIS_USER,
|
||||
password=self.config.IRIS_PASSWORD,
|
||||
)
|
||||
|
||||
def get_connection(self) -> Any:
|
||||
"""Get a connection from pool or create new if available."""
|
||||
with self._lock:
|
||||
if self._pool:
|
||||
conn = self._pool.pop()
|
||||
self._in_use += 1
|
||||
return conn
|
||||
if self._in_use < self._max_size:
|
||||
conn = self._create_connection()
|
||||
self._in_use += 1
|
||||
return conn
|
||||
raise RuntimeError("Connection pool exhausted")
|
||||
|
||||
def return_connection(self, conn: Any) -> None:
|
||||
"""Return connection to pool after validating it."""
|
||||
if not conn:
|
||||
return
|
||||
|
||||
# Validate connection health
|
||||
is_valid = False
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.close()
|
||||
is_valid = True
|
||||
except (OSError, RuntimeError) as e:
|
||||
logger.debug("Connection validation failed: %s", e)
|
||||
try:
|
||||
conn.close()
|
||||
except (OSError, RuntimeError):
|
||||
pass
|
||||
|
||||
with self._lock:
|
||||
self._pool.append(conn if is_valid else self._create_connection())
|
||||
self._in_use -= 1
|
||||
|
||||
def ensure_schema_exists(self, schema: str) -> None:
|
||||
"""Ensure schema exists in IRIS database.
|
||||
|
||||
This method is idempotent and thread-safe. It uses a memory cache to avoid
|
||||
redundant database queries for already-verified schemas.
|
||||
|
||||
Args:
|
||||
schema: Schema name to ensure exists
|
||||
|
||||
Raises:
|
||||
Exception: If schema creation fails
|
||||
"""
|
||||
# Fast path: check cache first (no lock needed for read-only set lookup)
|
||||
if schema in self._schemas_initialized:
|
||||
return
|
||||
|
||||
# Slow path: acquire lock and check again (double-checked locking)
|
||||
with self._lock:
|
||||
if schema in self._schemas_initialized:
|
||||
return
|
||||
|
||||
# Get a connection to check/create schema
|
||||
conn = self._pool[0] if self._pool else self._create_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Check if schema exists using INFORMATION_SCHEMA
|
||||
check_sql = """
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA
|
||||
WHERE SCHEMA_NAME = ?
|
||||
"""
|
||||
cursor.execute(check_sql, (schema,)) # Must be tuple or list
|
||||
exists = cursor.fetchone()[0] > 0
|
||||
|
||||
if not exists:
|
||||
# Schema doesn't exist, create it
|
||||
cursor.execute(f"CREATE SCHEMA {schema}")
|
||||
conn.commit()
|
||||
logger.info("Created schema: %s", schema)
|
||||
else:
|
||||
logger.debug("Schema already exists: %s", schema)
|
||||
|
||||
# Add to cache to skip future checks
|
||||
self._schemas_initialized.add(schema)
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.exception("Failed to ensure schema %s exists", schema)
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def close_all(self) -> None:
|
||||
"""Close all connections (application shutdown only)."""
|
||||
with self._lock:
|
||||
for conn in self._pool:
|
||||
try:
|
||||
conn.close()
|
||||
except (OSError, RuntimeError):
|
||||
pass
|
||||
self._pool.clear()
|
||||
self._in_use = 0
|
||||
self._schemas_initialized.clear()
|
||||
|
||||
|
||||
class IrisVector(BaseVector):
|
||||
"""IRIS vector database implementation using native VECTOR type and HNSW indexing."""
|
||||
|
||||
def __init__(self, collection_name: str, config: IrisVectorConfig) -> None:
|
||||
super().__init__(collection_name)
|
||||
self.config = config
|
||||
self.table_name = f"embedding_{collection_name}".upper()
|
||||
self.schema = config.IRIS_SCHEMA or "dify"
|
||||
self.pool = get_iris_pool(config)
|
||||
|
||||
def get_type(self) -> str:
|
||||
return VectorType.IRIS
|
||||
|
||||
@contextmanager
|
||||
def _get_cursor(self):
|
||||
"""Context manager for database cursor with connection pooling."""
|
||||
conn = self.pool.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
yield cursor
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
self.pool.return_connection(conn)
|
||||
|
||||
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs) -> list[str]:
|
||||
dimension = len(embeddings[0])
|
||||
self._create_collection(dimension)
|
||||
return self.add_texts(texts, embeddings)
|
||||
|
||||
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **_kwargs) -> list[str]:
|
||||
"""Add documents with embeddings to the collection."""
|
||||
added_ids = []
|
||||
with self._get_cursor() as cursor:
|
||||
for i, doc in enumerate(documents):
|
||||
doc_id = doc.metadata.get("doc_id", str(uuid.uuid4())) if doc.metadata else str(uuid.uuid4())
|
||||
metadata = json.dumps(doc.metadata) if doc.metadata else "{}"
|
||||
embedding_str = json.dumps(embeddings[i])
|
||||
|
||||
sql = f"INSERT INTO {self.schema}.{self.table_name} (id, text, meta, embedding) VALUES (?, ?, ?, ?)"
|
||||
cursor.execute(sql, (doc_id, doc.page_content, metadata, embedding_str))
|
||||
added_ids.append(doc_id)
|
||||
|
||||
return added_ids
|
||||
|
||||
def text_exists(self, id: str) -> bool: # pylint: disable=redefined-builtin
|
||||
try:
|
||||
with self._get_cursor() as cursor:
|
||||
sql = f"SELECT 1 FROM {self.schema}.{self.table_name} WHERE id = ?"
|
||||
cursor.execute(sql, (id,))
|
||||
return cursor.fetchone() is not None
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
return False
|
||||
|
||||
def delete_by_ids(self, ids: list[str]) -> None:
|
||||
if not ids:
|
||||
return
|
||||
|
||||
with self._get_cursor() as cursor:
|
||||
placeholders = ",".join(["?" for _ in ids])
|
||||
sql = f"DELETE FROM {self.schema}.{self.table_name} WHERE id IN ({placeholders})"
|
||||
cursor.execute(sql, ids)
|
||||
|
||||
def delete_by_metadata_field(self, key: str, value: str) -> None:
|
||||
"""Delete documents by metadata field (JSON LIKE pattern matching)."""
|
||||
with self._get_cursor() as cursor:
|
||||
pattern = f'%"{key}": "{value}"%'
|
||||
sql = f"DELETE FROM {self.schema}.{self.table_name} WHERE meta LIKE ?"
|
||||
cursor.execute(sql, (pattern,))
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
"""Search similar documents using VECTOR_COSINE with HNSW index."""
|
||||
top_k = kwargs.get("top_k", 4)
|
||||
score_threshold = float(kwargs.get("score_threshold") or 0.0)
|
||||
embedding_str = json.dumps(query_vector)
|
||||
|
||||
with self._get_cursor() as cursor:
|
||||
sql = f"""
|
||||
SELECT TOP {top_k} id, text, meta, VECTOR_COSINE(embedding, ?) as score
|
||||
FROM {self.schema}.{self.table_name}
|
||||
ORDER BY score DESC
|
||||
"""
|
||||
cursor.execute(sql, (embedding_str,))
|
||||
|
||||
docs = []
|
||||
for row in cursor.fetchall():
|
||||
if len(row) >= 4:
|
||||
text, meta_str, score = row[1], row[2], float(row[3])
|
||||
if score >= score_threshold:
|
||||
metadata = json.loads(meta_str) if meta_str else {}
|
||||
metadata["score"] = score
|
||||
docs.append(Document(page_content=text, metadata=metadata))
|
||||
return docs
|
||||
|
||||
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||
"""Search documents by full-text using iFind index or fallback to LIKE search."""
|
||||
top_k = kwargs.get("top_k", 5)
|
||||
|
||||
with self._get_cursor() as cursor:
|
||||
if self.config.IRIS_TEXT_INDEX:
|
||||
# Use iFind full-text search with index
|
||||
text_index_name = f"idx_{self.table_name}_text"
|
||||
sql = f"""
|
||||
SELECT TOP {top_k} id, text, meta
|
||||
FROM {self.schema}.{self.table_name}
|
||||
WHERE %ID %FIND search_index({text_index_name}, ?)
|
||||
"""
|
||||
cursor.execute(sql, (query,))
|
||||
else:
|
||||
# Fallback to LIKE search (inefficient for large datasets)
|
||||
query_pattern = f"%{query}%"
|
||||
sql = f"""
|
||||
SELECT TOP {top_k} id, text, meta
|
||||
FROM {self.schema}.{self.table_name}
|
||||
WHERE text LIKE ?
|
||||
"""
|
||||
cursor.execute(sql, (query_pattern,))
|
||||
|
||||
docs = []
|
||||
for row in cursor.fetchall():
|
||||
if len(row) >= 3:
|
||||
metadata = json.loads(row[2]) if row[2] else {}
|
||||
docs.append(Document(page_content=row[1], metadata=metadata))
|
||||
|
||||
if not docs:
|
||||
logger.info("Full-text search for '%s' returned no results", query)
|
||||
|
||||
return docs
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the entire collection (drop table - permanent)."""
|
||||
with self._get_cursor() as cursor:
|
||||
sql = f"DROP TABLE {self.schema}.{self.table_name}"
|
||||
cursor.execute(sql)
|
||||
|
||||
def _create_collection(self, dimension: int) -> None:
|
||||
"""Create table with VECTOR column and HNSW index.
|
||||
|
||||
Uses Redis lock to prevent concurrent creation attempts across multiple
|
||||
API server instances (api, worker, worker_beat).
|
||||
"""
|
||||
cache_key = f"vector_indexing_{self._collection_name}"
|
||||
lock_name = f"{cache_key}_lock"
|
||||
|
||||
with redis_client.lock(lock_name, timeout=20): # pylint: disable=not-context-manager
|
||||
if redis_client.get(cache_key):
|
||||
return
|
||||
|
||||
# Ensure schema exists (idempotent, cached after first call)
|
||||
self.pool.ensure_schema_exists(self.schema)
|
||||
|
||||
with self._get_cursor() as cursor:
|
||||
# Create table with VECTOR column
|
||||
sql = f"""
|
||||
CREATE TABLE {self.schema}.{self.table_name} (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
text CLOB,
|
||||
meta CLOB,
|
||||
embedding VECTOR(DOUBLE, {dimension})
|
||||
)
|
||||
"""
|
||||
logger.info("Creating table: %s.%s", self.schema, self.table_name)
|
||||
cursor.execute(sql)
|
||||
|
||||
# Create HNSW index for vector similarity search
|
||||
index_name = f"idx_{self.table_name}_embedding"
|
||||
sql_index = (
|
||||
f"CREATE INDEX {index_name} ON {self.schema}.{self.table_name} "
|
||||
"(embedding) AS HNSW(Distance='Cosine')"
|
||||
)
|
||||
logger.info("Creating HNSW index: %s", index_name)
|
||||
cursor.execute(sql_index)
|
||||
logger.info("HNSW index created successfully: %s", index_name)
|
||||
|
||||
# Create full-text search index if enabled
|
||||
logger.info(
|
||||
"IRIS_TEXT_INDEX config value: %s (type: %s)",
|
||||
self.config.IRIS_TEXT_INDEX,
|
||||
type(self.config.IRIS_TEXT_INDEX),
|
||||
)
|
||||
if self.config.IRIS_TEXT_INDEX:
|
||||
text_index_name = f"idx_{self.table_name}_text"
|
||||
language = self.config.IRIS_TEXT_INDEX_LANGUAGE
|
||||
# Fixed: Removed extra parentheses and corrected syntax
|
||||
sql_text_index = f"""
|
||||
CREATE INDEX {text_index_name} ON {self.schema}.{self.table_name} (text)
|
||||
AS %iFind.Index.Basic
|
||||
(LANGUAGE = '{language}', LOWER = 1, INDEXOPTION = 0)
|
||||
"""
|
||||
logger.info("Creating text index: %s with language: %s", text_index_name, language)
|
||||
logger.info("SQL for text index: %s", sql_text_index)
|
||||
cursor.execute(sql_text_index)
|
||||
logger.info("Text index created successfully: %s", text_index_name)
|
||||
else:
|
||||
logger.warning("Text index creation skipped - IRIS_TEXT_INDEX is disabled")
|
||||
|
||||
redis_client.set(cache_key, 1, ex=3600)
|
||||
|
||||
|
||||
class IrisVectorFactory(AbstractVectorFactory):
|
||||
"""Factory for creating IrisVector instances."""
|
||||
|
||||
def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> IrisVector:
|
||||
if dataset.index_struct_dict:
|
||||
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
|
||||
collection_name = class_prefix
|
||||
else:
|
||||
dataset_id = dataset.id
|
||||
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
||||
index_struct_dict = self.gen_index_struct_dict(VectorType.IRIS, collection_name)
|
||||
dataset.index_struct = json.dumps(index_struct_dict)
|
||||
|
||||
return IrisVector(
|
||||
collection_name=collection_name,
|
||||
config=IrisVectorConfig(
|
||||
IRIS_HOST=dify_config.IRIS_HOST,
|
||||
IRIS_SUPER_SERVER_PORT=dify_config.IRIS_SUPER_SERVER_PORT,
|
||||
IRIS_USER=dify_config.IRIS_USER,
|
||||
IRIS_PASSWORD=dify_config.IRIS_PASSWORD,
|
||||
IRIS_DATABASE=dify_config.IRIS_DATABASE,
|
||||
IRIS_SCHEMA=dify_config.IRIS_SCHEMA,
|
||||
IRIS_CONNECTION_URL=dify_config.IRIS_CONNECTION_URL,
|
||||
IRIS_MIN_CONNECTION=dify_config.IRIS_MIN_CONNECTION,
|
||||
IRIS_MAX_CONNECTION=dify_config.IRIS_MAX_CONNECTION,
|
||||
IRIS_TEXT_INDEX=dify_config.IRIS_TEXT_INDEX,
|
||||
IRIS_TEXT_INDEX_LANGUAGE=dify_config.IRIS_TEXT_INDEX_LANGUAGE,
|
||||
),
|
||||
)
|
||||
|
|
@ -187,6 +187,10 @@ class Vector:
|
|||
from core.rag.datasource.vdb.clickzetta.clickzetta_vector import ClickzettaVectorFactory
|
||||
|
||||
return ClickzettaVectorFactory
|
||||
case VectorType.IRIS:
|
||||
from core.rag.datasource.vdb.iris.iris_vector import IrisVectorFactory
|
||||
|
||||
return IrisVectorFactory
|
||||
case _:
|
||||
raise ValueError(f"Vector store {vector_type} is not supported.")
|
||||
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@ class VectorType(StrEnum):
|
|||
HUAWEI_CLOUD = "huawei_cloud"
|
||||
MATRIXONE = "matrixone"
|
||||
CLICKZETTA = "clickzetta"
|
||||
IRIS = "iris"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ class ToolFileMessageTransformer:
|
|||
meta = message.meta or {}
|
||||
|
||||
mimetype = meta.get("mime_type", "application/octet-stream")
|
||||
if not mimetype:
|
||||
mimetype = "application/octet-stream"
|
||||
# get filename from meta
|
||||
filename = meta.get("filename", None)
|
||||
# if message is str, encode it to bytes
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ vdb = [
|
|||
"pymochow==2.2.9",
|
||||
"pyobvector~=0.2.17",
|
||||
"qdrant-client==1.9.0",
|
||||
"intersystems-irispython>=5.1.0",
|
||||
"tablestore==6.3.7",
|
||||
"tcvectordb~=1.6.4",
|
||||
"tidb-vector==0.0.9",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import logging
|
|||
|
||||
from celery import shared_task
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from models import Account
|
||||
from services.billing_service import BillingService
|
||||
|
|
@ -14,7 +15,8 @@ logger = logging.getLogger(__name__)
|
|||
def delete_account_task(account_id):
|
||||
account = db.session.query(Account).where(Account.id == account_id).first()
|
||||
try:
|
||||
BillingService.delete_account(account_id)
|
||||
if dify_config.BILLING_ENABLED:
|
||||
BillingService.delete_account(account_id)
|
||||
except Exception:
|
||||
logger.exception("Failed to delete account %s from billing service.", account_id)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
|||
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
||||
|
||||
# Vector database configuration
|
||||
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase
|
||||
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, iris
|
||||
VECTOR_STORE=weaviate
|
||||
# Weaviate configuration
|
||||
WEAVIATE_ENDPOINT=http://localhost:8080
|
||||
|
|
@ -64,6 +64,20 @@ WEAVIATE_GRPC_ENABLED=false
|
|||
WEAVIATE_BATCH_SIZE=100
|
||||
WEAVIATE_TOKENIZATION=word
|
||||
|
||||
# InterSystems IRIS configuration
|
||||
IRIS_HOST=localhost
|
||||
IRIS_SUPER_SERVER_PORT=1972
|
||||
IRIS_WEB_SERVER_PORT=52773
|
||||
IRIS_USER=_SYSTEM
|
||||
IRIS_PASSWORD=Dify@1234
|
||||
IRIS_DATABASE=USER
|
||||
IRIS_SCHEMA=dify
|
||||
IRIS_CONNECTION_URL=
|
||||
IRIS_MIN_CONNECTION=1
|
||||
IRIS_MAX_CONNECTION=3
|
||||
IRIS_TEXT_INDEX=true
|
||||
IRIS_TEXT_INDEX_LANGUAGE=en
|
||||
|
||||
|
||||
# Upload configuration
|
||||
UPLOAD_FILE_SIZE_LIMIT=15
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
"""Integration tests for IRIS vector database."""
|
||||
|
||||
from core.rag.datasource.vdb.iris.iris_vector import IrisVector, IrisVectorConfig
|
||||
from tests.integration_tests.vdb.test_vector_store import (
|
||||
AbstractVectorTest,
|
||||
setup_mock_redis,
|
||||
)
|
||||
|
||||
|
||||
class IrisVectorTest(AbstractVectorTest):
|
||||
"""Test suite for IRIS vector store implementation."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize IRIS vector test with hardcoded test configuration.
|
||||
|
||||
Note: Uses 'host.docker.internal' to connect from DevContainer to
|
||||
host OS Docker, or 'localhost' when running directly on host OS.
|
||||
"""
|
||||
super().__init__()
|
||||
self.vector = IrisVector(
|
||||
collection_name=self.collection_name,
|
||||
config=IrisVectorConfig(
|
||||
IRIS_HOST="host.docker.internal",
|
||||
IRIS_SUPER_SERVER_PORT=1972,
|
||||
IRIS_USER="_SYSTEM",
|
||||
IRIS_PASSWORD="Dify@1234",
|
||||
IRIS_DATABASE="USER",
|
||||
IRIS_SCHEMA="dify",
|
||||
IRIS_CONNECTION_URL=None,
|
||||
IRIS_MIN_CONNECTION=1,
|
||||
IRIS_MAX_CONNECTION=3,
|
||||
IRIS_TEXT_INDEX=True,
|
||||
IRIS_TEXT_INDEX_LANGUAGE="en",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_iris_vector(setup_mock_redis) -> None:
|
||||
"""Run all IRIS vector store tests.
|
||||
|
||||
Args:
|
||||
setup_mock_redis: Pytest fixture for mock Redis setup
|
||||
"""
|
||||
IrisVectorTest().run_all_tests()
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import pytest
|
||||
|
||||
import core.tools.utils.message_transformer as mt
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
|
||||
|
||||
class _FakeToolFile:
|
||||
def __init__(self, mimetype: str):
|
||||
self.id = "fake-tool-file-id"
|
||||
self.mimetype = mimetype
|
||||
|
||||
|
||||
class _FakeToolFileManager:
|
||||
"""Fake ToolFileManager to capture the mimetype passed in."""
|
||||
|
||||
last_call: dict | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def create_file_by_raw(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
tenant_id: str,
|
||||
conversation_id: str | None,
|
||||
file_binary: bytes,
|
||||
mimetype: str,
|
||||
filename: str | None = None,
|
||||
):
|
||||
type(self).last_call = {
|
||||
"user_id": user_id,
|
||||
"tenant_id": tenant_id,
|
||||
"conversation_id": conversation_id,
|
||||
"file_binary": file_binary,
|
||||
"mimetype": mimetype,
|
||||
"filename": filename,
|
||||
}
|
||||
return _FakeToolFile(mimetype)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_tool_file_manager(monkeypatch):
|
||||
# Patch the manager used inside the transformer module
|
||||
monkeypatch.setattr(mt, "ToolFileManager", _FakeToolFileManager)
|
||||
# also ensure predictable URL generation (no need to patch; uses id and extension only)
|
||||
yield
|
||||
_FakeToolFileManager.last_call = None
|
||||
|
||||
|
||||
def _gen(messages):
|
||||
yield from messages
|
||||
|
||||
|
||||
def test_transform_tool_invoke_messages_mimetype_key_present_but_none():
|
||||
# Arrange: a BLOB message whose meta contains a mime_type key set to None
|
||||
blob = b"hello"
|
||||
msg = ToolInvokeMessage(
|
||||
type=ToolInvokeMessage.MessageType.BLOB,
|
||||
message=ToolInvokeMessage.BlobMessage(blob=blob),
|
||||
meta={"mime_type": None, "filename": "greeting"},
|
||||
)
|
||||
|
||||
# Act
|
||||
out = list(
|
||||
mt.ToolFileMessageTransformer.transform_tool_invoke_messages(
|
||||
messages=_gen([msg]),
|
||||
user_id="u1",
|
||||
tenant_id="t1",
|
||||
conversation_id="c1",
|
||||
)
|
||||
)
|
||||
|
||||
# Assert: default to application/octet-stream when mime_type is present but None
|
||||
assert _FakeToolFileManager.last_call is not None
|
||||
assert _FakeToolFileManager.last_call["mimetype"] == "application/octet-stream"
|
||||
|
||||
# Should yield a BINARY_LINK (not IMAGE_LINK) and the URL ends with .bin
|
||||
assert len(out) == 1
|
||||
o = out[0]
|
||||
assert o.type == ToolInvokeMessage.MessageType.BINARY_LINK
|
||||
assert isinstance(o.message, ToolInvokeMessage.TextMessage)
|
||||
assert o.message.text.endswith(".bin")
|
||||
# meta is preserved (still contains mime_type: None)
|
||||
assert "mime_type" in (o.meta or {})
|
||||
assert o.meta["mime_type"] is None
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
Unit tests for delete_account_task.
|
||||
|
||||
Covers:
|
||||
- Billing enabled with existing account: calls billing and sends success email
|
||||
- Billing disabled with existing account: skips billing, sends success email
|
||||
- Account not found: still calls billing when enabled, does not send email
|
||||
- Billing deletion raises: logs and re-raises, no email
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tasks.delete_account_task import delete_account_task
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Mock the db.session used in delete_account_task."""
|
||||
with patch("tasks.delete_account_task.db.session") as mock_session:
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.where.return_value = mock_query
|
||||
yield mock_session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_deps():
|
||||
"""Patch external dependencies: BillingService and send_deletion_success_task."""
|
||||
with (
|
||||
patch("tasks.delete_account_task.BillingService") as mock_billing,
|
||||
patch("tasks.delete_account_task.send_deletion_success_task") as mock_mail_task,
|
||||
):
|
||||
# ensure .delay exists on the mail task
|
||||
mock_mail_task.delay = MagicMock()
|
||||
yield {
|
||||
"billing": mock_billing,
|
||||
"mail_task": mock_mail_task,
|
||||
}
|
||||
|
||||
|
||||
def _set_account_found(mock_db_session, email: str = "user@example.com"):
|
||||
account = SimpleNamespace(email=email)
|
||||
mock_db_session.query.return_value.where.return_value.first.return_value = account
|
||||
return account
|
||||
|
||||
|
||||
def _set_account_missing(mock_db_session):
|
||||
mock_db_session.query.return_value.where.return_value.first.return_value = None
|
||||
|
||||
|
||||
class TestDeleteAccountTask:
|
||||
def test_billing_enabled_account_exists_calls_billing_and_sends_email(self, mock_db_session, mock_deps):
|
||||
# Arrange
|
||||
account_id = "acc-123"
|
||||
account = _set_account_found(mock_db_session, email="a@b.com")
|
||||
|
||||
# Enable billing
|
||||
with patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True):
|
||||
# Act
|
||||
delete_account_task(account_id)
|
||||
|
||||
# Assert
|
||||
mock_deps["billing"].delete_account.assert_called_once_with(account_id)
|
||||
mock_deps["mail_task"].delay.assert_called_once_with(account.email)
|
||||
|
||||
def test_billing_disabled_account_exists_sends_email_only(self, mock_db_session, mock_deps):
|
||||
# Arrange
|
||||
account_id = "acc-456"
|
||||
account = _set_account_found(mock_db_session, email="x@y.com")
|
||||
|
||||
# Disable billing
|
||||
with patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", False):
|
||||
# Act
|
||||
delete_account_task(account_id)
|
||||
|
||||
# Assert
|
||||
mock_deps["billing"].delete_account.assert_not_called()
|
||||
mock_deps["mail_task"].delay.assert_called_once_with(account.email)
|
||||
|
||||
def test_account_not_found_billing_enabled_calls_billing_no_email(self, mock_db_session, mock_deps, caplog):
|
||||
# Arrange
|
||||
account_id = "missing-id"
|
||||
_set_account_missing(mock_db_session)
|
||||
|
||||
# Enable billing
|
||||
with patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True):
|
||||
# Act
|
||||
delete_account_task(account_id)
|
||||
|
||||
# Assert
|
||||
mock_deps["billing"].delete_account.assert_called_once_with(account_id)
|
||||
mock_deps["mail_task"].delay.assert_not_called()
|
||||
# Optional: verify log contains not found message
|
||||
assert any("not found" in rec.getMessage().lower() for rec in caplog.records)
|
||||
|
||||
def test_billing_delete_raises_propagates_and_no_email(self, mock_db_session, mock_deps):
|
||||
# Arrange
|
||||
account_id = "acc-err"
|
||||
_set_account_found(mock_db_session, email="err@ex.com")
|
||||
mock_deps["billing"].delete_account.side_effect = RuntimeError("billing down")
|
||||
|
||||
# Enable billing
|
||||
with patch("tasks.delete_account_task.dify_config.BILLING_ENABLED", True):
|
||||
# Act & Assert
|
||||
with pytest.raises(RuntimeError):
|
||||
delete_account_task(account_id)
|
||||
|
||||
# Ensure email was not sent
|
||||
mock_deps["mail_task"].delay.assert_not_called()
|
||||
4630
api/uv.lock
4630
api/uv.lock
File diff suppressed because it is too large
Load Diff
|
|
@ -518,7 +518,7 @@ SUPABASE_URL=your-server-url
|
|||
# ------------------------------
|
||||
|
||||
# The type of vector store to use.
|
||||
# Supported values are `weaviate`, `oceanbase`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`, `clickzetta`, `alibabacloud_mysql`.
|
||||
# Supported values are `weaviate`, `oceanbase`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`, `clickzetta`, `alibabacloud_mysql`, `iris`.
|
||||
VECTOR_STORE=weaviate
|
||||
# Prefix used to create collection name in vector database
|
||||
VECTOR_INDEX_NAME_PREFIX=Vector_index
|
||||
|
|
@ -792,6 +792,21 @@ CLICKZETTA_ANALYZER_TYPE=chinese
|
|||
CLICKZETTA_ANALYZER_MODE=smart
|
||||
CLICKZETTA_VECTOR_DISTANCE_FUNCTION=cosine_distance
|
||||
|
||||
# InterSystems IRIS configuration, only available when VECTOR_STORE is `iris`
|
||||
IRIS_HOST=iris
|
||||
IRIS_SUPER_SERVER_PORT=1972
|
||||
IRIS_WEB_SERVER_PORT=52773
|
||||
IRIS_USER=_SYSTEM
|
||||
IRIS_PASSWORD=Dify@1234
|
||||
IRIS_DATABASE=USER
|
||||
IRIS_SCHEMA=dify
|
||||
IRIS_CONNECTION_URL=
|
||||
IRIS_MIN_CONNECTION=1
|
||||
IRIS_MAX_CONNECTION=3
|
||||
IRIS_TEXT_INDEX=true
|
||||
IRIS_TEXT_INDEX_LANGUAGE=en
|
||||
IRIS_TIMEZONE=UTC
|
||||
|
||||
# ------------------------------
|
||||
# Knowledge Configuration
|
||||
# ------------------------------
|
||||
|
|
|
|||
|
|
@ -648,6 +648,26 @@ services:
|
|||
CHROMA_SERVER_AUTHN_PROVIDER: ${CHROMA_SERVER_AUTHN_PROVIDER:-chromadb.auth.token_authn.TokenAuthenticationServerProvider}
|
||||
IS_PERSISTENT: ${CHROMA_IS_PERSISTENT:-TRUE}
|
||||
|
||||
# InterSystems IRIS vector database
|
||||
iris:
|
||||
image: containers.intersystems.com/intersystems/iris-community:2025.3
|
||||
profiles:
|
||||
- iris
|
||||
container_name: iris
|
||||
restart: always
|
||||
init: true
|
||||
ports:
|
||||
- "${IRIS_SUPER_SERVER_PORT:-1972}:1972"
|
||||
- "${IRIS_WEB_SERVER_PORT:-52773}:52773"
|
||||
volumes:
|
||||
- ./volumes/iris:/opt/iris
|
||||
- ./iris/iris-init.script:/iris-init.script
|
||||
- ./iris/docker-entrypoint.sh:/custom-entrypoint.sh
|
||||
entrypoint: ["/custom-entrypoint.sh"]
|
||||
tty: true
|
||||
environment:
|
||||
TZ: ${IRIS_TIMEZONE:-UTC}
|
||||
|
||||
# Oracle vector database
|
||||
oracle:
|
||||
image: container-registry.oracle.com/database/free:latest
|
||||
|
|
|
|||
|
|
@ -361,6 +361,19 @@ x-shared-env: &shared-api-worker-env
|
|||
CLICKZETTA_ANALYZER_TYPE: ${CLICKZETTA_ANALYZER_TYPE:-chinese}
|
||||
CLICKZETTA_ANALYZER_MODE: ${CLICKZETTA_ANALYZER_MODE:-smart}
|
||||
CLICKZETTA_VECTOR_DISTANCE_FUNCTION: ${CLICKZETTA_VECTOR_DISTANCE_FUNCTION:-cosine_distance}
|
||||
IRIS_HOST: ${IRIS_HOST:-iris}
|
||||
IRIS_SUPER_SERVER_PORT: ${IRIS_SUPER_SERVER_PORT:-1972}
|
||||
IRIS_WEB_SERVER_PORT: ${IRIS_WEB_SERVER_PORT:-52773}
|
||||
IRIS_USER: ${IRIS_USER:-_SYSTEM}
|
||||
IRIS_PASSWORD: ${IRIS_PASSWORD:-Dify@1234}
|
||||
IRIS_DATABASE: ${IRIS_DATABASE:-USER}
|
||||
IRIS_SCHEMA: ${IRIS_SCHEMA:-dify}
|
||||
IRIS_CONNECTION_URL: ${IRIS_CONNECTION_URL:-}
|
||||
IRIS_MIN_CONNECTION: ${IRIS_MIN_CONNECTION:-1}
|
||||
IRIS_MAX_CONNECTION: ${IRIS_MAX_CONNECTION:-3}
|
||||
IRIS_TEXT_INDEX: ${IRIS_TEXT_INDEX:-true}
|
||||
IRIS_TEXT_INDEX_LANGUAGE: ${IRIS_TEXT_INDEX_LANGUAGE:-en}
|
||||
IRIS_TIMEZONE: ${IRIS_TIMEZONE:-UTC}
|
||||
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
|
||||
UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5}
|
||||
UPLOAD_FILE_EXTENSION_BLACKLIST: ${UPLOAD_FILE_EXTENSION_BLACKLIST:-}
|
||||
|
|
@ -1286,6 +1299,26 @@ services:
|
|||
CHROMA_SERVER_AUTHN_PROVIDER: ${CHROMA_SERVER_AUTHN_PROVIDER:-chromadb.auth.token_authn.TokenAuthenticationServerProvider}
|
||||
IS_PERSISTENT: ${CHROMA_IS_PERSISTENT:-TRUE}
|
||||
|
||||
# InterSystems IRIS vector database
|
||||
iris:
|
||||
image: containers.intersystems.com/intersystems/iris-community:2025.3
|
||||
profiles:
|
||||
- iris
|
||||
container_name: iris
|
||||
restart: always
|
||||
init: true
|
||||
ports:
|
||||
- "${IRIS_SUPER_SERVER_PORT:-1972}:1972"
|
||||
- "${IRIS_WEB_SERVER_PORT:-52773}:52773"
|
||||
volumes:
|
||||
- ./volumes/iris:/opt/iris
|
||||
- ./iris/iris-init.script:/iris-init.script
|
||||
- ./iris/docker-entrypoint.sh:/custom-entrypoint.sh
|
||||
entrypoint: ["/custom-entrypoint.sh"]
|
||||
tty: true
|
||||
environment:
|
||||
TZ: ${IRIS_TIMEZONE:-UTC}
|
||||
|
||||
# Oracle vector database
|
||||
oracle:
|
||||
image: container-registry.oracle.com/database/free:latest
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# IRIS configuration flag file
|
||||
IRIS_CONFIG_DONE="/opt/iris/.iris-configured"
|
||||
|
||||
# Function to configure IRIS
|
||||
configure_iris() {
|
||||
echo "Configuring IRIS for first-time setup..."
|
||||
|
||||
# Wait for IRIS to be fully started
|
||||
sleep 5
|
||||
|
||||
# Execute the initialization script
|
||||
iris session IRIS < /iris-init.script
|
||||
|
||||
# Mark configuration as done
|
||||
touch "$IRIS_CONFIG_DONE"
|
||||
|
||||
echo "IRIS configuration completed."
|
||||
}
|
||||
|
||||
# Start IRIS in background for initial configuration if not already configured
|
||||
if [ ! -f "$IRIS_CONFIG_DONE" ]; then
|
||||
echo "First-time IRIS setup detected. Starting IRIS for configuration..."
|
||||
|
||||
# Start IRIS
|
||||
iris start IRIS
|
||||
|
||||
# Configure IRIS
|
||||
configure_iris
|
||||
|
||||
# Stop IRIS
|
||||
iris stop IRIS quietly
|
||||
fi
|
||||
|
||||
# Run the original IRIS entrypoint
|
||||
exec /iris-main "$@"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Switch to the %SYS namespace to modify system settings
|
||||
set $namespace="%SYS"
|
||||
|
||||
// Set predefined user passwords to never expire (default password: SYS)
|
||||
Do ##class(Security.Users).UnExpireUserPasswords("*")
|
||||
|
||||
// Change the default password
|
||||
Do $SYSTEM.Security.ChangePassword("_SYSTEM","Dify@1234")
|
||||
|
||||
// Install the Japanese locale (default is English since the container is Ubuntu-based)
|
||||
// Do ##class(Config.NLS.Locales).Install("jpuw")
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# Cursor Rules for Dify Project
|
||||
|
||||
## Automated Test Generation
|
||||
|
||||
- Use `web/testing/testing.md` as the canonical instruction set for generating frontend automated tests.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -53,7 +53,7 @@ const Popup: FC<PopupProps> = ({
|
|||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 1000 }}>
|
||||
<div className='max-w-[360px] rounded-xl bg-background-section-burn shadow-lg'>
|
||||
<div className='max-w-[360px] rounded-xl bg-background-section-burn shadow-lg backdrop-blur-[5px]'>
|
||||
<div className='px-4 pb-2 pt-3'>
|
||||
<div className='flex h-[18px] items-center'>
|
||||
<FileIcon type={fileType} className='mr-1 h-4 w-4 shrink-0' />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import AppCard, { type AppCardProps } from './index'
|
||||
import type { App } from '@/models/explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/app-icon', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: any) => <div data-testid="app-icon">{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../app/type-selector', () => ({
|
||||
AppTypeIcon: ({ type }: any) => <div data-testid="app-type-icon">{type}</div>,
|
||||
}))
|
||||
|
||||
const createApp = (overrides?: Partial<App>): App => ({
|
||||
app_id: 'app-id',
|
||||
description: 'App description',
|
||||
copyright: '2024',
|
||||
privacy_policy: null,
|
||||
custom_disclaimer: null,
|
||||
category: 'Assistant',
|
||||
position: 1,
|
||||
is_listed: true,
|
||||
install_count: 0,
|
||||
installed: false,
|
||||
editable: true,
|
||||
is_agent: false,
|
||||
...overrides,
|
||||
app: {
|
||||
id: 'id-1',
|
||||
mode: AppModeEnum.CHAT,
|
||||
icon_type: null,
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
icon_url: '',
|
||||
name: 'Sample App',
|
||||
description: 'App description',
|
||||
use_icon_as_answer_icon: false,
|
||||
...overrides?.app,
|
||||
},
|
||||
})
|
||||
|
||||
describe('AppCard', () => {
|
||||
const onCreate = jest.fn()
|
||||
|
||||
const renderComponent = (props?: Partial<AppCardProps>) => {
|
||||
const mergedProps: AppCardProps = {
|
||||
app: createApp(),
|
||||
canCreate: false,
|
||||
onCreate,
|
||||
isExplore: false,
|
||||
...props,
|
||||
}
|
||||
return render(<AppCard {...mergedProps} />)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render app info with correct mode label when mode is CHAT', () => {
|
||||
renderComponent({ app: createApp({ app: { ...createApp().app, mode: AppModeEnum.CHAT } }) })
|
||||
|
||||
expect(screen.getByText('Sample App')).toBeInTheDocument()
|
||||
expect(screen.getByText('App description')).toBeInTheDocument()
|
||||
expect(screen.getByText('APP.TYPES.CHATBOT')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-type-icon')).toHaveTextContent(AppModeEnum.CHAT)
|
||||
})
|
||||
|
||||
it('should show create button in explore mode and trigger action', () => {
|
||||
renderComponent({
|
||||
app: createApp({ app: { ...createApp().app, mode: AppModeEnum.WORKFLOW } }),
|
||||
canCreate: true,
|
||||
isExplore: true,
|
||||
})
|
||||
|
||||
const button = screen.getByText('explore.appCard.addToWorkspace')
|
||||
expect(button).toBeInTheDocument()
|
||||
fireEvent.click(button)
|
||||
expect(onCreate).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByText('APP.TYPES.WORKFLOW')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide create button when not allowed', () => {
|
||||
renderComponent({ canCreate: false, isExplore: true })
|
||||
|
||||
expect(screen.queryByText('explore.appCard.addToWorkspace')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -187,7 +187,7 @@ const PluginTasks = () => {
|
|||
{/* Running Plugins */}
|
||||
{runningPlugins.length > 0 && (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1'>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary'>
|
||||
{t('plugin.task.installing')} ({runningPlugins.length})
|
||||
</div>
|
||||
<div className='max-h-[200px] overflow-y-auto'>
|
||||
|
|
@ -220,7 +220,7 @@ const PluginTasks = () => {
|
|||
{/* Success Plugins */}
|
||||
{successPlugins.length > 0 && (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1'>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary'>
|
||||
{t('plugin.task.installed')} ({successPlugins.length})
|
||||
<Button
|
||||
className='shrink-0'
|
||||
|
|
@ -261,7 +261,7 @@ const PluginTasks = () => {
|
|||
{/* Error Plugins */}
|
||||
{errorPlugins.length > 0 && (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1'>
|
||||
<div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1 text-text-secondary'>
|
||||
{t('plugin.task.installError', { errorLength: errorPlugins.length })}
|
||||
<Button
|
||||
className='shrink-0'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import NoData from './index'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('NoData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
it('should render empty state icon and text when mounted', () => {
|
||||
const { container } = render(<NoData />)
|
||||
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
expect(screen.getByText('share.generation.noData')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -37,9 +37,13 @@ import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
|||
|
||||
type WorkflowChecklistProps = {
|
||||
disabled: boolean
|
||||
showGoTo?: boolean
|
||||
onItemClick?: (item: ChecklistItem) => void
|
||||
}
|
||||
const WorkflowChecklist = ({
|
||||
disabled,
|
||||
showGoTo = true,
|
||||
onItemClick,
|
||||
}: WorkflowChecklistProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -49,9 +53,13 @@ const WorkflowChecklist = ({
|
|||
const { handleNodeSelect } = useNodesInteractions()
|
||||
|
||||
const handleChecklistItemClick = (item: ChecklistItem) => {
|
||||
if (!item.canNavigate)
|
||||
const goToEnabled = showGoTo && item.canNavigate && !item.disableGoTo
|
||||
if (!goToEnabled)
|
||||
return
|
||||
handleNodeSelect(item.id)
|
||||
if (onItemClick)
|
||||
onItemClick(item)
|
||||
else
|
||||
handleNodeSelect(item.id)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +124,7 @@ const WorkflowChecklist = ({
|
|||
key={node.id}
|
||||
className={cn(
|
||||
'group mb-2 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs last-of-type:mb-0',
|
||||
node.canNavigate ? 'cursor-pointer' : 'cursor-default opacity-80',
|
||||
showGoTo && node.canNavigate && !node.disableGoTo ? 'cursor-pointer' : 'cursor-default opacity-80',
|
||||
)}
|
||||
onClick={() => handleChecklistItemClick(node)}
|
||||
>
|
||||
|
|
@ -130,7 +138,7 @@ const WorkflowChecklist = ({
|
|||
{node.title}
|
||||
</span>
|
||||
{
|
||||
node.canNavigate && (
|
||||
(showGoTo && node.canNavigate && !node.disableGoTo) && (
|
||||
<div className='flex h-4 w-[60px] shrink-0 items-center justify-center gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100'>
|
||||
<span className='whitespace-nowrap text-xs font-medium leading-4 text-primary-600'>
|
||||
{t('workflow.panel.goTo')}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export type ChecklistItem = {
|
|||
unConnected?: boolean
|
||||
errorMessage?: string
|
||||
canNavigate: boolean
|
||||
disableGoTo?: boolean
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
|
|
@ -75,6 +76,13 @@ const START_NODE_TYPES: BlockEnum[] = [
|
|||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
// Node types that depend on plugins
|
||||
const PLUGIN_DEPENDENT_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Tool,
|
||||
BlockEnum.DataSource,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
|
|
@ -157,7 +165,14 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
|||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid
|
||||
let errorMessage = validator ? validator(checkData, t, moreDataForCheckValid).errorMessage : undefined
|
||||
const isPluginMissing = PLUGIN_DEPENDENT_TYPES.includes(node.data.type as BlockEnum) && node.data._pluginInstallLocked
|
||||
|
||||
// Check if plugin is installed for plugin-dependent nodes first
|
||||
let errorMessage: string | undefined
|
||||
if (isPluginMissing)
|
||||
errorMessage = t('workflow.nodes.common.pluginNotInstalled')
|
||||
else if (validator)
|
||||
errorMessage = validator(checkData, t, moreDataForCheckValid).errorMessage
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
|
@ -194,7 +209,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
|||
toolIcon,
|
||||
unConnected: isUnconnected && !canSkipConnectionCheck,
|
||||
errorMessage,
|
||||
canNavigate: true,
|
||||
canNavigate: !isPluginMissing,
|
||||
disableGoTo: isPluginMissing,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
nodeId={nodeId}
|
||||
isSupportPromptGenerator={!!def.auto_generate?.type}
|
||||
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
|
||||
editorContainerClassName='px-0'
|
||||
editorContainerClassName='px-0 bg-components-input-bg-normal focus-within:bg-components-input-bg-active rounded-lg'
|
||||
availableNodes={availableNodes}
|
||||
nodesOutputVars={nodeOutputVars}
|
||||
isSupportJinja={def.template?.enabled}
|
||||
|
|
@ -108,7 +108,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
}
|
||||
placeholderClassName='px-2 py-1'
|
||||
titleClassName='system-sm-semibold-uppercase text-text-secondary text-[13px]'
|
||||
inputClassName='px-2 py-1 bg-components-input-bg-normal focus:bg-components-input-bg-active focus:border-components-input-border-active focus:border rounded-lg'
|
||||
inputClassName='px-2 py-1'
|
||||
/>
|
||||
}
|
||||
case FormTypeEnum.textNumber: {
|
||||
|
|
|
|||
|
|
@ -185,6 +185,14 @@ export default combine(
|
|||
sonarjs: sonar,
|
||||
},
|
||||
},
|
||||
// allow generated i18n files (like i18n/*/workflow.ts) to exceed max-lines
|
||||
{
|
||||
files: ['i18n/**'],
|
||||
rules: {
|
||||
'sonarjs/max-lines': 'off',
|
||||
'max-lines': 'off',
|
||||
},
|
||||
},
|
||||
// need further research
|
||||
{
|
||||
rules: {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export type I18nText = {
|
|||
'uk-UA': string
|
||||
'id-ID': string
|
||||
'tr-TR': string
|
||||
'fa-IR': string
|
||||
'ar-TN': string
|
||||
'YOUR_LANGUAGE_CODE': string
|
||||
}
|
||||
```
|
||||
|
|
@ -157,6 +159,18 @@ export const languages = [
|
|||
example: 'Привет, Dify!',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
value: 'fa-IR',
|
||||
name: 'Farsi (Iran)',
|
||||
example: 'سلام, دیفای!',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
value: 'ar-TN',
|
||||
name: 'العربية (تونس)',
|
||||
example: 'مرحبا، Dify!',
|
||||
supported: true,
|
||||
},
|
||||
// Add your language here 👇
|
||||
...
|
||||
// Add your language here 👆
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export type I18nText = {
|
|||
'tr-TR': string
|
||||
'fa-IR': string
|
||||
'sl-SI': string
|
||||
'ar-TN': string
|
||||
}
|
||||
|
||||
export const languages = data.languages
|
||||
|
|
@ -81,6 +82,7 @@ export const NOTICE_I18N = {
|
|||
tr_TR: 'Önemli Duyuru',
|
||||
fa_IR: 'هشدار مهم',
|
||||
sl_SI: 'Pomembno obvestilo',
|
||||
ar_TN: 'إشعار مهم',
|
||||
},
|
||||
desc: {
|
||||
en_US:
|
||||
|
|
@ -117,6 +119,8 @@ export const NOTICE_I18N = {
|
|||
'Naš sistem ne bo na voljo od 19:00 do 24:00 UTC 28. avgusta zaradi nadgradnje. Za vprašanja se obrnite na našo skupino za podporo (support@dify.ai). Cenimo vašo potrpežljivost.',
|
||||
th_TH:
|
||||
'ระบบของเราจะไม่สามารถใช้งานได้ตั้งแต่เวลา 19:00 ถึง 24:00 UTC ในวันที่ 28 สิงหาคม เพื่อทำการอัปเกรด หากมีคำถามใดๆ กรุณาติดต่อทีมสนับสนุนของเรา (support@dify.ai) เราขอขอบคุณในความอดทนของท่าน',
|
||||
ar_TN:
|
||||
'سيكون نظامنا غير متاح من الساعة 19:00 إلى 24:00 بالتوقيت العالمي المنسق في 28 أغسطس لإجراء ترقية. للأسئلة، يرجى الاتصال بفريق الدعم لدينا (support@dify.ai). نحن نقدر صبرك.',
|
||||
},
|
||||
href: '#',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@
|
|||
"prompt_name": "Indonesian",
|
||||
"example": "Halo, Dify!",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"value": "ar-TN",
|
||||
"name": "العربية (تونس)",
|
||||
"prompt_name": "Tunisian Arabic",
|
||||
"example": "مرحبا، Dify!",
|
||||
"supported": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
const translation = {
|
||||
title: 'التعليقات التوضيحية',
|
||||
name: 'رد التعليق التوضيحي',
|
||||
editBy: 'تم تعديل الإجابة بواسطة {{author}}',
|
||||
noData: {
|
||||
title: 'لا توجد تعليقات توضيحية',
|
||||
description: 'يمكنك تعديل التعليقات التوضيحية أثناء تصحيح أخطاء التطبيق أو استيراد التعليقات التوضيحية بالجملة هنا للحصول على استجابة عالية الجودة.',
|
||||
},
|
||||
table: {
|
||||
header: {
|
||||
question: 'السؤال',
|
||||
answer: 'الإجابة',
|
||||
createdAt: 'تم الإنشاء في',
|
||||
hits: 'المطابقات',
|
||||
actions: 'الإجراءات',
|
||||
addAnnotation: 'إضافة تعليق توضيحي',
|
||||
bulkImport: 'استيراد بالجملة',
|
||||
bulkExport: 'تصدير بالجملة',
|
||||
clearAll: 'حذف الكل',
|
||||
clearAllConfirm: 'حذف جميع التعليقات التوضيحية؟',
|
||||
},
|
||||
},
|
||||
editModal: {
|
||||
title: 'تعديل رد التعليق التوضيحي',
|
||||
queryName: 'استعلام المستخدم',
|
||||
answerName: 'الراوي',
|
||||
yourAnswer: 'إجابتك',
|
||||
answerPlaceholder: 'اكتب إجابتك هنا',
|
||||
yourQuery: 'استعلامك',
|
||||
queryPlaceholder: 'اكتب استعلامك هنا',
|
||||
removeThisCache: 'حذف هذا التعليق التوضيحي',
|
||||
createdAt: 'تم الإنشاء في',
|
||||
},
|
||||
addModal: {
|
||||
title: 'إضافة رد تعليق توضيحي',
|
||||
queryName: 'السؤال',
|
||||
answerName: 'الإجابة',
|
||||
answerPlaceholder: 'اكتب الإجابة هنا',
|
||||
queryPlaceholder: 'اكتب الاستعلام هنا',
|
||||
createNext: 'إضافة رد توضيحي آخر',
|
||||
},
|
||||
batchModal: {
|
||||
title: 'استيراد بالجملة',
|
||||
csvUploadTitle: 'اسحب وأفلت ملف CSV هنا، أو ',
|
||||
browse: 'تصفح',
|
||||
tip: 'يجب أن يتوافق ملف CSV مع الهيكل التالي:',
|
||||
question: 'السؤال',
|
||||
answer: 'الإجابة',
|
||||
contentTitle: 'محتوى المقطع',
|
||||
content: 'المحتوى',
|
||||
template: 'تحميل القالب من هنا',
|
||||
cancel: 'إلغاء',
|
||||
run: 'تشغيل الدفعة',
|
||||
runError: 'فشل تشغيل الدفعة',
|
||||
processing: 'جاري المعالجة',
|
||||
completed: 'اكتمل الاستيراد',
|
||||
error: 'خطأ في الاستيراد',
|
||||
ok: 'موافق',
|
||||
},
|
||||
list: {
|
||||
delete: {
|
||||
title: 'هل أنت متأكد من الحذف؟',
|
||||
},
|
||||
},
|
||||
batchAction: {
|
||||
selected: 'المحدد',
|
||||
delete: 'حذف',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
errorMessage: {
|
||||
answerRequired: 'الإجابة مطلوبة',
|
||||
queryRequired: 'السؤال مطلوب',
|
||||
},
|
||||
viewModal: {
|
||||
annotatedResponse: 'رد التعليق التوضيحي',
|
||||
hitHistory: 'سجل المطابقة',
|
||||
hit: 'مطابقة',
|
||||
hits: 'مطابقات',
|
||||
noHitHistory: 'لا يوجد سجل مطابقة',
|
||||
},
|
||||
hitHistoryTable: {
|
||||
query: 'الاستعلام',
|
||||
match: 'المطابقة',
|
||||
response: 'الاستجابة',
|
||||
source: 'المصدر',
|
||||
score: 'النتيجة',
|
||||
time: 'الوقت',
|
||||
},
|
||||
initSetup: {
|
||||
title: 'الإعداد الأولي لرد التعليق التوضيحي',
|
||||
configTitle: 'إعداد رد التعليق التوضيحي',
|
||||
confirmBtn: 'حفظ وتمكين',
|
||||
configConfirmBtn: 'حفظ',
|
||||
},
|
||||
embeddingModelSwitchTip: 'سيؤدي تبديل نموذج التضمين للنص التوضيحي إلى إعادة التضمين، مما يؤدي إلى تكاليف إضافية.',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
const translation = {
|
||||
apiServer: 'خادم API',
|
||||
apiKey: 'مفتاح API',
|
||||
status: 'الحالة',
|
||||
disabled: 'معطل',
|
||||
ok: 'في الخدمة',
|
||||
copy: 'نسخ',
|
||||
copied: 'تم النسخ',
|
||||
regenerate: 'إعادة إنشاء',
|
||||
play: 'تشغيل',
|
||||
pause: 'إيقاف مؤقت',
|
||||
playing: 'جاري التشغيل',
|
||||
loading: 'جاري التحميل',
|
||||
merMaid: {
|
||||
rerender: 'إعادة الرسم',
|
||||
},
|
||||
never: 'أبدا',
|
||||
apiKeyModal: {
|
||||
apiSecretKey: 'مفتاح API السري',
|
||||
apiSecretKeyTips: 'لمنع إساءة استخدام API، قم بحماية مفتاح API الخاص بك. تجنب استخدامه كنص عادي في كود الواجهة الأمامية. :)',
|
||||
createNewSecretKey: 'إنشاء مفتاح سري جديد',
|
||||
secretKey: 'المفتاح السري',
|
||||
created: 'تم الإنشاء',
|
||||
lastUsed: 'آخر استخدام',
|
||||
generateTips: 'احتفظ بهذا المفتاح في مكان آمن ويمكن الوصول إليه.',
|
||||
},
|
||||
actionMsg: {
|
||||
deleteConfirmTitle: 'حذف هذا المفتاح السري؟',
|
||||
deleteConfirmTips: 'لا يمكن التراجع عن هذا الإجراء.',
|
||||
ok: 'موافق',
|
||||
},
|
||||
completionMode: {
|
||||
title: 'API تطبيق الإكمال',
|
||||
info: 'لتوليد نصوص عالية الجودة، مثل المقالات والملخصات والترجمات، استخدم API رسائل الإكمال مع إدخال المستخدم. يعتمد توليد النص على معلمات النموذج وقوالب المطالبة المعينة في هندسة مطالبات Dify.',
|
||||
createCompletionApi: 'إنشاء رسالة إكمال',
|
||||
createCompletionApiTip: 'إنشاء رسالة إكمال لدعم وضع السؤال والجواب.',
|
||||
inputsTips: '(اختياري) توفير حقول إدخال المستخدم كأزواج مفتاح وقيمة، بما يتوافق مع المتغيرات في هندسة المطالبات. المفتاح هو اسم المتغير، والقيمة هي قيمة المعلمة. إذا كان نوع الحقل هو تحديد، فيجب أن تكون القيمة المرسلة واحدة من الخيارات المحددة مسبقًا.',
|
||||
queryTips: 'محتوى نص إدخال المستخدم.',
|
||||
blocking: 'نوع الحظر، في انتظار اكتمال التنفيذ وإرجاع النتائج. (قد يتم قطع الطلبات إذا كانت العملية طويلة)',
|
||||
streaming: 'عائدات التدفق. تنفيذ عائد التدفق بناءً على SSE (أحداث مرسلة من الخادم).',
|
||||
messageFeedbackApi: 'ملاحظات الرسالة (إعجاب)',
|
||||
messageFeedbackApiTip: 'قيم الرسائل المستلمة نيابة عن المستخدمين النهائيين بإعجاب أو عدم إعجاب. هذه البيانات مرئية في صفحة السجلات والتعليقات التوضيحية وتستخدم لضبط النموذج في المستقبل.',
|
||||
messageIDTip: 'معرف الرسالة',
|
||||
ratingTip: 'إعجاب أو عدم إعجاب، null للإلغاء',
|
||||
parametersApi: 'الحصول على معلومات حول معلمات التطبيق',
|
||||
parametersApiTip: 'استرداد معلمات الإدخال المكونة، بما في ذلك أسماء المتغيرات وأسماء الحقول والأنواع والقيم الافتراضية. تستخدم عادة لعرض هذه الحقول في نموذج أو ملء القيم الافتراضية بعد تحميل العميل.',
|
||||
},
|
||||
chatMode: {
|
||||
title: 'API تطبيق الدردشة',
|
||||
info: 'للتطبيقات المحادثة متعددة الاستخدامات باستخدام تنسيق Q&A، اتصل بـ API رسائل الدردشة لبدء الحوار. حافظ على المحادثات الجارية عن طريق تمرير conversation_id المرتجع. تعتمد معلمات الاستجابة والقوالب على إعدادات Dify Prompt Eng.',
|
||||
createChatApi: 'إنشاء رسالة دردشة',
|
||||
createChatApiTip: 'بناء رسالة محادثة جديدة أو استمرار حوار موجود.',
|
||||
inputsTips: '(اختياري) توفير حقول إدخال المستخدم كأزواج مفتاح وقيمة، بما يتوافق مع المتغيرات في هندسة المطالبات. المفتاح هو اسم المتغير، والقيمة هي قيمة المعلمة. إذا كان نوع الحقل هو تحديد، فيجب أن تكون القيمة المرسلة واحدة من الخيارات المحددة مسبقًا.',
|
||||
queryTips: 'محتوى إدخال/سؤال المستخدم',
|
||||
blocking: 'نوع الحظر، في انتظار اكتمال التنفيذ وإرجاع النتائج. (قد يتم قطع الطلبات إذا كانت العملية طويلة)',
|
||||
streaming: 'عائدات التدفق. تنفيذ عائد التدفق بناءً على SSE (أحداث مرسلة من الخادم).',
|
||||
conversationIdTip: '(اختياري) معرف المحادثة: اتركه فارغًا للمحادثة لأول مرة؛ مرر conversation_id من السياق لمتابعة الحوار.',
|
||||
messageFeedbackApi: 'ملاحظات مستخدم محطة الرسالة، إعجاب',
|
||||
messageFeedbackApiTip: 'قيم الرسائل المستلمة نيابة عن المستخدمين النهائيين بإعجاب أو عدم إعجاب. هذه البيانات مرئية في صفحة السجلات والتعليقات التوضيحية وتستخدم لضبط النموذج في المستقبل.',
|
||||
messageIDTip: 'معرف الرسالة',
|
||||
ratingTip: 'إعجاب أو عدم إعجاب، null للإلغاء',
|
||||
chatMsgHistoryApi: 'الحصول على رسالة سجل الدردشة',
|
||||
chatMsgHistoryApiTip: 'تُرجع الصفحة الأولى أحدث شريط `limit`، وهو بترتيب عكسي.',
|
||||
chatMsgHistoryConversationIdTip: 'معرف المحادثة',
|
||||
chatMsgHistoryFirstId: 'معرف سجل الدردشة الأول في الصفحة الحالية. الافتراضي هو لا شيء.',
|
||||
chatMsgHistoryLimit: 'كم عدد المحادثات التي يتم إرجاعها في طلب واحد',
|
||||
conversationsListApi: 'الحصول على قائمة المحادثات',
|
||||
conversationsListApiTip: 'يحصل على قائمة الجلسات للمستخدم الحالي. بشكل افتراضي، يتم إرجاع آخر 20 جلسة.',
|
||||
conversationsListFirstIdTip: 'معرف السجل الأخير في الصفحة الحالية، الافتراضي لا شيء.',
|
||||
conversationsListLimitTip: 'كم عدد المحادثات التي يتم إرجاعها في طلب واحد',
|
||||
conversationRenamingApi: 'إعادة تسمية المحادثة',
|
||||
conversationRenamingApiTip: 'إعادة تسمية المحادثات؛ يتم عرض الاسم في واجهات العملاء متعددة الجلسات.',
|
||||
conversationRenamingNameTip: 'اسم جديد',
|
||||
parametersApi: 'الحصول على معلومات حول معلمات التطبيق',
|
||||
parametersApiTip: 'استرداد معلمات الإدخال المكونة، بما في ذلك أسماء المتغيرات وأسماء الحقول والأنواع والقيم الافتراضية. تستخدم عادة لعرض هذه الحقول في نموذج أو ملء القيم الافتراضية بعد تحميل العميل.',
|
||||
},
|
||||
develop: {
|
||||
requestBody: 'جسم الطلب (Request Body)',
|
||||
pathParams: 'معلمات المسار (Path Params)',
|
||||
query: 'استعلام (Query)',
|
||||
toc: 'المحتويات',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
const translation = {
|
||||
pageTitle: {
|
||||
line1: 'المطالبة',
|
||||
line2: 'الهندسة',
|
||||
},
|
||||
orchestrate: 'تنسيق',
|
||||
promptMode: {
|
||||
simple: 'التبديل إلى وضع الخبير لتعديل المطالبة بالكامل',
|
||||
advanced: 'وضع الخبير',
|
||||
switchBack: 'التبديل مرة أخرى',
|
||||
advancedWarning: {
|
||||
title: 'لقد انتقلت إلى وضع الخبير، وبمجرد تعديل المطالبة، لا يمكنك العودة إلى الوضع الأساسي.',
|
||||
description: 'في وضع الخبير، يمكنك تعديل المطالبة بالكامل.',
|
||||
learnMore: 'اعرف المزيد',
|
||||
ok: 'موافق',
|
||||
},
|
||||
operation: {
|
||||
addMessage: 'إضافة رسالة',
|
||||
},
|
||||
contextMissing: 'مكون السياق مفقود، قد لا تكون فعالية المطالبة جيدة.',
|
||||
},
|
||||
operation: {
|
||||
applyConfig: 'نشر',
|
||||
resetConfig: 'إعادة تعيين',
|
||||
debugConfig: 'تصحيح',
|
||||
addFeature: 'إضافة ميزة',
|
||||
automatic: 'توليد',
|
||||
stopResponding: 'إيقاف الاستجابة',
|
||||
agree: 'إعجاب',
|
||||
disagree: 'لم يعجبني',
|
||||
cancelAgree: 'إلغاء الإعجاب',
|
||||
cancelDisagree: 'إلغاء عدم الإعجاب',
|
||||
userAction: 'المستخدم ',
|
||||
},
|
||||
notSetAPIKey: {
|
||||
title: 'لم يتم تعيين مفتاح مزود LLM',
|
||||
trailFinished: 'انتهت التجربة',
|
||||
description: 'لم يتم تعيين مفتاح مزود LLM، ويجب تعيينه قبل تصحيح الأخطاء.',
|
||||
settingBtn: 'الذهاب إلى الإعدادات',
|
||||
},
|
||||
trailUseGPT4Info: {
|
||||
title: 'لا يدعم gpt-4 الآن',
|
||||
description: 'لاستخدام gpt-4، يرجى تعيين مفتاح API.',
|
||||
},
|
||||
feature: {
|
||||
groupChat: {
|
||||
title: 'تحسين الدردشة',
|
||||
description: 'أضف إعدادات ما قبل المحادثة للتطبيقات يمكن أن يعزز تجربة المستخدم.',
|
||||
},
|
||||
groupExperience: {
|
||||
title: 'تحسين التجربة',
|
||||
},
|
||||
conversationOpener: {
|
||||
title: 'فاتحة المحادثة',
|
||||
description: 'في تطبيق الدردشة، يتم استخدام الجملة الأولى التي يتحدث بها الذكاء الاصطناعي بنشاط للمستخدم عادةً كترحيب.',
|
||||
},
|
||||
suggestedQuestionsAfterAnswer: {
|
||||
title: 'متابعة',
|
||||
description: 'يمكن أن يعطي إعداد اقتراح الأسئلة التالية للمستخدمين دردشة أفضل.',
|
||||
resDes: '3 اقتراحات للسؤال التالي للمستخدم.',
|
||||
tryToAsk: 'حاول أن تسأل',
|
||||
},
|
||||
moreLikeThis: {
|
||||
title: 'المزيد مثل هذا',
|
||||
description: 'توليد نصوص متعددة في وقت واحد، ثم تحريرها ومتابعة التوليد',
|
||||
generateNumTip: 'عدد مرات التوليد لكل مرة',
|
||||
tip: 'سيؤدي استخدام هذه الميزة إلى تكبد نفقات إضافية للرموز',
|
||||
},
|
||||
speechToText: {
|
||||
title: 'تحويل الكلام إلى نص',
|
||||
description: 'يمكن استخدام الإدخال الصوتي في الدردشة.',
|
||||
resDes: 'تم تمكين الإدخال الصوتي',
|
||||
},
|
||||
textToSpeech: {
|
||||
title: 'تحويل النص إلى كلام',
|
||||
description: 'يمكن تحويل رسائل المحادثة إلى كلام.',
|
||||
resDes: 'تم تمكين تحويل النص إلى صوت',
|
||||
},
|
||||
citation: {
|
||||
title: 'الاقتباسات والسمات',
|
||||
description: 'عرض المستند المصدري والقسم المنسوب للمحتوى المولد.',
|
||||
resDes: 'تم تمكين الاقتباسات والسمات',
|
||||
},
|
||||
annotation: {
|
||||
title: 'رد التعليق التوضيحي',
|
||||
description: 'يمكنك إضافة استجابة عالية الجودة يدويًا إلى ذاكرة التخزين المؤقت للمطابقة ذات الأولوية مع أسئلة المستخدم المماثلة.',
|
||||
resDes: 'تم تمكين استجابة التعليق التوضيحي',
|
||||
scoreThreshold: {
|
||||
title: 'عتبة النتيجة',
|
||||
description: 'يستخدم لتعيين عتبة التشابه لرد التعليق التوضيحي.',
|
||||
easyMatch: 'تطابق سهل',
|
||||
accurateMatch: 'تطابق دقيق',
|
||||
},
|
||||
matchVariable: {
|
||||
title: 'متغير المطابقة',
|
||||
choosePlaceholder: 'اختر متغير المطابقة',
|
||||
},
|
||||
cacheManagement: 'التعليقات التوضيحية',
|
||||
cached: 'تم التعليق',
|
||||
remove: 'إزالة',
|
||||
removeConfirm: 'حذف هذا التعليق التوضيحي؟',
|
||||
add: 'إضافة تعليق توضيحي',
|
||||
edit: 'تعديل التعليق التوضيحي',
|
||||
},
|
||||
dataSet: {
|
||||
title: 'المعرفة',
|
||||
noData: 'يمكنك استيراد المعرفة كسياق',
|
||||
selectTitle: 'حدد المعرفة المرجعية',
|
||||
selected: 'تم تحديد المعرفة',
|
||||
noDataSet: 'لم يتم العثور على معرفة',
|
||||
toCreate: 'الذهاب للإنشاء',
|
||||
notSupportSelectMulti: 'دعم معرفة واحدة فقط حاليًا',
|
||||
queryVariable: {
|
||||
title: 'متغير الاستعلام',
|
||||
tip: 'سيتم استخدام هذا المتغير كمدخل استعلام لاسترجاع السياق، والحصول على معلومات السياق المتعلقة بمدخل هذا المتغير.',
|
||||
choosePlaceholder: 'اختر متغير الاستعلام',
|
||||
noVar: 'لا توجد متغيرات',
|
||||
noVarTip: 'يرجى إنشاء متغير في قسم المتغيرات',
|
||||
unableToQueryDataSet: 'غير قادر على استعلام المعرفة',
|
||||
unableToQueryDataSetTip: 'غير قادر على استعلام المعرفة بنجاح، يرجى اختيار متغير استعلام سياق في قسم السياق.',
|
||||
ok: 'موافق',
|
||||
contextVarNotEmpty: 'لا يمكن أن يكون متغير استعلام السياق فارغًا',
|
||||
deleteContextVarTitle: 'حذف المتغير "{{varName}}"؟',
|
||||
deleteContextVarTip: 'تم تعيين هذا المتغير كمتغير استعلام سياق، وسيؤثر إزالته على الاستخدام العادي للمعرفة. إذا كنت لا تزال بحاجة إلى حذفه، يرجى إعادة تحديده في قسم السياق.',
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
title: 'الأدوات',
|
||||
tips: 'توفر الأدوات طريقة استدعاء API قياسية، مع أخذ مدخلات المستخدم أو المتغيرات كمعلمات طلب للاستعلام عن البيانات الخارجية كسياق.',
|
||||
toolsInUse: '{{count}} أدوات قيد الاستخدام',
|
||||
modal: {
|
||||
title: 'أداة',
|
||||
toolType: {
|
||||
title: 'نوع الأداة',
|
||||
placeholder: 'يرجى اختيار نوع الأداة',
|
||||
},
|
||||
name: {
|
||||
title: 'الاسم',
|
||||
placeholder: 'يرجى إدخال الاسم',
|
||||
},
|
||||
variableName: {
|
||||
title: 'اسم المتغير',
|
||||
placeholder: 'يرجى إدخال اسم المتغير',
|
||||
},
|
||||
},
|
||||
},
|
||||
conversationHistory: {
|
||||
title: 'سجل المحادثة',
|
||||
description: 'تعيين أسماء بادئة لأدوار المحادثة',
|
||||
tip: 'لم يتم تمكين سجل المحادثة، يرجى إضافة <histories> في المطالبة أعلاه.',
|
||||
learnMore: 'اعرف المزيد',
|
||||
editModal: {
|
||||
title: 'تعديل أسماء أدوار المحادثة',
|
||||
userPrefix: 'بادئة المستخدم',
|
||||
assistantPrefix: 'بادئة المساعد',
|
||||
},
|
||||
},
|
||||
toolbox: {
|
||||
title: 'صندوق الأدوات',
|
||||
},
|
||||
moderation: {
|
||||
title: 'تعديل المحتوى',
|
||||
description: 'تأمين إخراج النموذج باستخدام API التعديل أو الحفاظ على قائمة كلمات حساسة.',
|
||||
contentEnableLabel: 'تم تمكين تعديل المحتوى',
|
||||
allEnabled: 'الإدخال والإخراج',
|
||||
inputEnabled: 'الإدخال',
|
||||
outputEnabled: 'الإخراج',
|
||||
modal: {
|
||||
title: 'إعدادات تعديل المحتوى',
|
||||
provider: {
|
||||
title: 'المزود',
|
||||
openai: 'OpenAI Moderation',
|
||||
openaiTip: {
|
||||
prefix: 'تتطلب OpenAI Moderation مفتاح OpenAI API تم تكوينه في ',
|
||||
suffix: '.',
|
||||
},
|
||||
keywords: 'الكلمات الرئيسية',
|
||||
},
|
||||
keywords: {
|
||||
tip: 'واحد لكل سطر، مفصولة بفواصل الأسطر. ما يصل إلى 100 حرف لكل سطر.',
|
||||
placeholder: 'واحد لكل سطر، مفصولة بفواصل الأسطر',
|
||||
line: 'سطر',
|
||||
},
|
||||
content: {
|
||||
input: 'تعديل محتوى الإدخال',
|
||||
output: 'تعديل محتوى الإخراج',
|
||||
preset: 'ردود محددة مسبقًا',
|
||||
placeholder: 'محتوى الردود المحددة مسبقًا هنا',
|
||||
condition: 'تم تمكين تعديل محتوى الإدخال والإخراج واحد على الأقل',
|
||||
fromApi: 'يتم إرجاع الردود المحددة مسبقًا بواسطة API',
|
||||
errorMessage: 'لا يمكن أن تكون الردود المحددة مسبقًا فارغة',
|
||||
supportMarkdown: 'دعم Markdown',
|
||||
},
|
||||
openaiNotConfig: {
|
||||
before: 'تتطلب OpenAI Moderation مفتاح OpenAI API تم تكوينه في',
|
||||
after: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
fileUpload: {
|
||||
title: 'تحميل الملف',
|
||||
description: 'يسمح مربع إدخال الدردشة بتحميل الصور والمستندات والملفات الأخرى.',
|
||||
supportedTypes: 'أنواع الملفات المدعومة',
|
||||
numberLimit: 'الحد الأقصى للتحميلات',
|
||||
modalTitle: 'إعداد تحميل الملف',
|
||||
},
|
||||
imageUpload: {
|
||||
title: 'تحميل الصور',
|
||||
description: 'السماح بتحميل الصور.',
|
||||
supportedTypes: 'أنواع الملفات المدعومة',
|
||||
numberLimit: 'الحد الأقصى للتحميلات',
|
||||
modalTitle: 'إعداد تحميل الصور',
|
||||
},
|
||||
bar: {
|
||||
empty: 'تمكين الميزة لتعزيز تجربة مستخدم تطبيق الويب',
|
||||
enableText: 'تم تمكين الميزات',
|
||||
manage: 'إدارة',
|
||||
},
|
||||
documentUpload: {
|
||||
title: 'مستند',
|
||||
description: 'سيسمح تمكين المستند للنموذج بأخذ المستندات والإجابة على الأسئلة حولها.',
|
||||
},
|
||||
audioUpload: {
|
||||
title: 'صوت',
|
||||
description: 'سيسمح تمكين الصوت للنموذج بمعالجة ملفات الصوت للنسخ والتحليل.',
|
||||
},
|
||||
},
|
||||
codegen: {
|
||||
title: 'مولد الكود',
|
||||
description: 'يستخدم مولد الكود النماذج المكونة لتوليد كود عالي الجودة بناءً على تعليماتك. يرجى تقديم تعليمات واضحة ومفصلة.',
|
||||
instruction: 'تعليمات',
|
||||
instructionPlaceholder: 'أدخل وصفًا تفصيليًا للكود الذي تريد توليده.',
|
||||
noDataLine1: 'صف حالة استخدامك على اليسار،',
|
||||
noDataLine2: 'سيظهر معاينة الكود هنا.',
|
||||
generate: 'توليد',
|
||||
generatedCodeTitle: 'الكود المولد',
|
||||
loading: 'جاري توليد الكود...',
|
||||
apply: 'تطبيق',
|
||||
applyChanges: 'تطبيق التغييرات',
|
||||
resTitle: 'الكود المولد',
|
||||
overwriteConfirmTitle: 'استبدال الكود الموجود؟',
|
||||
overwriteConfirmMessage: 'سيؤدي هذا الإجراء إلى استبدال الكود الموجود. هل تريد المتابعة؟',
|
||||
},
|
||||
generate: {
|
||||
title: 'مولد المطالبة',
|
||||
description: 'يستخدم مولد المطالبة النموذج المكون لتحسين المطالبات للحصول على جودة أعلى وبنية أفضل. يرجى كتابة تعليمات واضحة ومفصلة.',
|
||||
tryIt: 'جربه',
|
||||
instruction: 'تعليمات',
|
||||
instructionPlaceHolderTitle: 'صف كيف ترغب في تحسين هذه المطالبة. على سبيل المثال:',
|
||||
instructionPlaceHolderLine1: 'اجعل الإخراج أكثر إيجازًا، مع الاحتفاظ بالنقاط الأساسية.',
|
||||
instructionPlaceHolderLine2: 'تنسيق الإخراج غير صحيح، يرجى اتباع تنسيق JSON بدقة.',
|
||||
instructionPlaceHolderLine3: 'النبرة قاسية جدًا، يرجى جعلها أكثر ودية.',
|
||||
codeGenInstructionPlaceHolderLine: 'كلما كانت الملاحظات أكثر تفصيلاً، مثل أنواع بيانات الإدخال والإخراج وكذلك كيفية معالجة المتغيرات، كلما كان توليد الكود أكثر دقة.',
|
||||
idealOutput: 'المخرجات المثالية',
|
||||
idealOutputPlaceholder: 'صف تنسيق الاستجابة المثالي، والطول، والنبرة، ومتطلبات المحتوى...',
|
||||
optional: 'اختياري',
|
||||
dismiss: 'تجاهل',
|
||||
generate: 'توليد',
|
||||
resTitle: 'المطالبة المولدة',
|
||||
newNoDataLine1: 'اكتب تعليمات في العمود الأيسر، وانقر فوق توليد لرؤية الاستجابة. ',
|
||||
apply: 'تطبيق',
|
||||
loading: 'تنسيق التطبيق لك...',
|
||||
overwriteTitle: 'تجاوز التكوين الحالي؟',
|
||||
overwriteMessage: 'سيؤدي تطبيق هذه المطالبة إلى تجاوز التكوين الحالي.',
|
||||
template: {
|
||||
pythonDebugger: {
|
||||
name: 'مصحح أخطاء بايثون',
|
||||
instruction: 'برنامج روبوت يمكنه إنشاء وتصحيح الكود الخاص بك بناءً على تعليماتك',
|
||||
},
|
||||
translation: {
|
||||
name: 'ترجمة',
|
||||
instruction: 'مترجم يمكنه ترجمة لغات متعددة',
|
||||
},
|
||||
professionalAnalyst: {
|
||||
name: 'محلل محترف',
|
||||
instruction: 'استخراج الرؤى وتحديد المخاطر وتقطير المعلومات الأساسية من التقارير الطويلة في مذكرة واحدة',
|
||||
},
|
||||
excelFormulaExpert: {
|
||||
name: 'خبير صيغة Excel',
|
||||
instruction: 'روبوت دردشة يمكنه مساعدة المستخدمين المبتدئين على فهم صيغ Excel واستخدامها وإنشائها بناءً على تعليمات المستخدم',
|
||||
},
|
||||
travelPlanning: {
|
||||
name: 'تخطيط السفر',
|
||||
instruction: 'مساعد تخطيط السفر هو أداة ذكية مصممة لمساعدة المستخدمين على التخطيط لرحلاتهم بسهولة',
|
||||
},
|
||||
SQLSorcerer: {
|
||||
name: 'ساحر SQL',
|
||||
instruction: 'تحويل اللغة اليومية إلى استعلامات SQL',
|
||||
},
|
||||
GitGud: {
|
||||
name: 'Git gud',
|
||||
instruction: 'إنشاء أوامر Git مناسبة بناءً على إجراءات التحكم في الإصدار التي وصفها المستخدم',
|
||||
},
|
||||
meetingTakeaways: {
|
||||
name: 'اجتماع الوجبات الجاهزة',
|
||||
instruction: 'تقطير الاجتماعات في ملخصات موجزة بما في ذلك مواضيع المناقشة، والوجبات الجاهزة الرئيسية، وعناصر العمل',
|
||||
},
|
||||
writingsPolisher: {
|
||||
name: 'ملمع الكتابات',
|
||||
instruction: 'استخدم تقنيات التحرير المتقدمة لتحسين كتاباتك',
|
||||
},
|
||||
},
|
||||
press: 'اضغط',
|
||||
to: 'إلى ',
|
||||
insertContext: 'إدراج السياق',
|
||||
optimizePromptTooltip: 'تحسين في مولد المطالبة',
|
||||
optimizationNote: 'ملاحظة التحسين',
|
||||
versions: 'إصدارات',
|
||||
version: 'إصدار',
|
||||
latest: 'الأحدث',
|
||||
},
|
||||
resetConfig: {
|
||||
title: 'تأكيد إعادة التعيين؟',
|
||||
message:
|
||||
'تتجاهل إعادة التعيين التغييرات، وتستعيد التكوين الأخير المنشور.',
|
||||
},
|
||||
errorMessage: {
|
||||
nameOfKeyRequired: 'اسم المفتاح: {{key}} مطلوب',
|
||||
valueOfVarRequired: 'قيمة {{key}} لا يمكن أن تكون فارغة',
|
||||
queryRequired: 'نص الطلب مطلوب.',
|
||||
waitForResponse: 'يرجى الانتظار حتى اكتمال الرد على الرسالة السابقة.',
|
||||
waitForBatchResponse: 'يرجى الانتظار حتى اكتمال الرد على مهمة الدفعة.',
|
||||
notSelectModel: 'يرجى اختيار نموذج',
|
||||
waitForImgUpload: 'يرجى الانتظار حتى تحميل الصورة',
|
||||
waitForFileUpload: 'يرجى الانتظار حتى تحميل الملف/الملفات',
|
||||
},
|
||||
warningMessage: {
|
||||
timeoutExceeded: 'لا يتم عرض النتائج بسبب المهلة. يرجى الرجوع إلى السجلات لجمع النتائج الكاملة.',
|
||||
},
|
||||
chatSubTitle: 'تعليمات',
|
||||
completionSubTitle: 'مقدمة المطالبة',
|
||||
promptTip:
|
||||
'توجه المطالبات استجابات الذكاء الاصطناعي بالتعليمات والقيود. أدرج متغيرات مثل {{input}}. لن تكون هذه المطالبة مرئية للمستخدمين.',
|
||||
formattingChangedTitle: 'تغيير التنسيق',
|
||||
formattingChangedText:
|
||||
'سيؤدي تعديل التنسيق إلى إعادة تعيين منطقة التصحيح، هل أنت متأكد؟',
|
||||
variableTitle: 'المتغيرات',
|
||||
variableTip:
|
||||
'يملأ المستخدمون المتغيرات في نموذج، ويستبدلون المتغيرات تلقائيًا في المطالبة.',
|
||||
notSetVar: 'تسمح المتغيرات للمستخدمين بتقديم كلمات مطالبة أو ملاحظات افتتاحية عند ملء النماذج. يمكنك محاولة إدخال "{{input}}" في كلمات المطالبة.',
|
||||
autoAddVar: 'المتغيرات غير المحددة المشار إليها في ما قبل المطالبة، هل ترغب في إضافتها في نموذج إدخال المستخدم؟',
|
||||
variableTable: {
|
||||
key: 'مفتاح المتغير',
|
||||
name: 'اسم حقل إدخال المستخدم',
|
||||
type: 'نوع الإدخال',
|
||||
action: 'إجراءات',
|
||||
typeString: 'سلسلة',
|
||||
typeSelect: 'تحديد',
|
||||
},
|
||||
varKeyError: {
|
||||
canNoBeEmpty: '{{key}} مطلوب',
|
||||
tooLong: '{{key}} طويل جدًا. لا يمكن أن يكون أطول من 30 حرفًا',
|
||||
notValid: '{{key}} غير صالح. يمكن أن يحتوي فقط على أحرف وأرقام وشرطات سفلية',
|
||||
notStartWithNumber: '{{key}} لا يمكن أن يبدأ برقم',
|
||||
keyAlreadyExists: '{{key}} موجود بالفعل',
|
||||
},
|
||||
otherError: {
|
||||
promptNoBeEmpty: 'لا يمكن أن تكون المطالبة فارغة',
|
||||
historyNoBeEmpty: 'يجب تعيين سجل المحادثة في المطالبة',
|
||||
queryNoBeEmpty: 'يجب تعيين الاستعلام في المطالبة',
|
||||
},
|
||||
variableConfig: {
|
||||
'addModalTitle': 'إضافة حقل إدخال',
|
||||
'editModalTitle': 'تعديل حقل إدخال',
|
||||
'description': 'إعداد للمتغير {{varName}}',
|
||||
'fieldType': 'نوع الحقل',
|
||||
'string': 'نص قصير',
|
||||
'text-input': 'نص قصير',
|
||||
'paragraph': 'فقرة',
|
||||
'select': 'تحديد',
|
||||
'number': 'رقم',
|
||||
'checkbox': 'مربع اختيار',
|
||||
'json': 'كود JSON',
|
||||
'jsonSchema': 'مخطط JSON',
|
||||
'optional': 'اختياري',
|
||||
'single-file': 'ملف واحد',
|
||||
'multi-files': 'قائمة ملفات',
|
||||
'notSet': 'لم يتم التعيين، حاول كتابة {{input}} في بادئة المطالبة',
|
||||
'stringTitle': 'خيارات مربع نص النموذج',
|
||||
'maxLength': 'الحد الأقصى للطول',
|
||||
'options': 'خيارات',
|
||||
'addOption': 'إضافة خيار',
|
||||
'apiBasedVar': 'متغير قائم على API',
|
||||
'varName': 'اسم المتغير',
|
||||
'labelName': 'اسم التسمية',
|
||||
'displayName': 'اسم العرض',
|
||||
'inputPlaceholder': 'يرجى الإدخال',
|
||||
'content': 'المحتوى',
|
||||
'required': 'مطلوب',
|
||||
'placeholder': 'عنصر نائب',
|
||||
'placeholderPlaceholder': 'أدخل نصًا للعرض عندما يكون الحقل فارغًا',
|
||||
'defaultValue': 'القيمة الافتراضية',
|
||||
'defaultValuePlaceholder': 'أدخل قيمة افتراضية لملء الحقل مسبقًا',
|
||||
'unit': 'وحدة',
|
||||
'unitPlaceholder': 'عرض الوحدات بعد الأرقام، مثل الرموز',
|
||||
'tooltips': 'تلميحات الأدوات',
|
||||
'tooltipsPlaceholder': 'أدخل نصًا مفيدًا يظهر عند التمرير فوق التسمية',
|
||||
'showAllSettings': 'عرض جميع الإعدادات',
|
||||
'startSelectedOption': 'بدء الخيار المحدد',
|
||||
'noDefaultSelected': 'لا تحدد',
|
||||
'hide': 'إخفاء',
|
||||
'file': {
|
||||
supportFileTypes: 'أنواع الملفات المدعومة',
|
||||
image: {
|
||||
name: 'صورة',
|
||||
},
|
||||
audio: {
|
||||
name: 'صوت',
|
||||
},
|
||||
document: {
|
||||
name: 'مستند',
|
||||
},
|
||||
video: {
|
||||
name: 'فيديو',
|
||||
},
|
||||
custom: {
|
||||
name: 'أنواع ملفات أخرى',
|
||||
description: 'تحديد أنواع ملفات أخرى.',
|
||||
createPlaceholder: '+ ملحق الملف، مثل .doc',
|
||||
},
|
||||
},
|
||||
'uploadFileTypes': 'تحميل أنواع الملفات',
|
||||
'uploadMethod': 'طريقة التحميل',
|
||||
'localUpload': 'تحميل محلي',
|
||||
'both': 'كلاهما',
|
||||
'maxNumberOfUploads': 'الحد الأقصى لعدد التحميلات',
|
||||
'maxNumberTip': 'وثيقة < {{docLimit}}، صورة < {{imgLimit}}، صوت < {{audioLimit}}، فيديو < {{videoLimit}}',
|
||||
'errorMsg': {
|
||||
labelNameRequired: 'اسم التسمية مطلوب',
|
||||
varNameCanBeRepeat: 'اسم المتغير لا يمكن تكراره',
|
||||
atLeastOneOption: 'خيار واحد على الأقل مطلوب',
|
||||
optionRepeat: 'يوجد خيارات مكررة',
|
||||
},
|
||||
'startChecked': 'البدء محددًا',
|
||||
'noDefaultValue': 'لا توجد قيمة افتراضية',
|
||||
'selectDefaultValue': 'تحديد القيمة الافتراضية',
|
||||
},
|
||||
vision: {
|
||||
name: 'الرؤية',
|
||||
description: 'سيسمح تمكين الرؤية للنموذج بأخذ الصور والإجابة على الأسئلة حولها. ',
|
||||
onlySupportVisionModelTip: 'يدعم نماذج الرؤية فقط',
|
||||
settings: 'الإعدادات',
|
||||
visionSettings: {
|
||||
title: 'إعدادات الرؤية',
|
||||
resolution: 'الدقة',
|
||||
resolutionTooltip: 'ستسمح الدقة المنخفضة للنموذج باستلام نسخة منخفضة الدقة 512 × 512 من الصورة، وتمثيل الصورة بميزانية 65 رمزًا. يتيح ذلك للواجهة البرمجية إرجاع استجابات أسرع واستهلاك عدد أقل من رموز الإدخال لحالات الاستخدام التي لا تتطلب تفاصيل عالية. ستسمح الدقة العالية أولاً للنموذج برؤية الصورة منخفضة الدقة ثم إنشاء مقتطفات مفصلة من الصور المدخلة كمربعات 512 بكسل بناءً على حجم الصورة المدخلة. يستخدم كل مقتطف مفصل ضعف ميزانية الرمز المميز بإجمالي 129 رمزًا.',
|
||||
high: 'عالية',
|
||||
low: 'منخفضة',
|
||||
uploadMethod: 'طريقة التحميل',
|
||||
both: 'كلاهما',
|
||||
localUpload: 'تحميل محلي',
|
||||
url: 'عنوان URL',
|
||||
uploadLimit: 'حد التحميل',
|
||||
},
|
||||
},
|
||||
voice: {
|
||||
name: 'صوت',
|
||||
defaultDisplay: 'صوت افتراضي',
|
||||
description: 'إعدادات تحويل النص إلى كلام',
|
||||
settings: 'الإعدادات',
|
||||
voiceSettings: {
|
||||
title: 'إعدادات الصوت',
|
||||
language: 'اللغة',
|
||||
resolutionTooltip: 'دعم لغة تحويل النص إلى كلام.',
|
||||
voice: 'صوت',
|
||||
autoPlay: 'تشغيل تلقائي',
|
||||
autoPlayEnabled: 'تشغيل',
|
||||
autoPlayDisabled: 'إيقاف',
|
||||
},
|
||||
},
|
||||
openingStatement: {
|
||||
title: 'فاتحة المحادثة',
|
||||
add: 'إضافة',
|
||||
writeOpener: 'تعديل الفاتحة',
|
||||
placeholder: 'اكتب رسالتك الافتتاحية هنا، يمكنك استخدام المتغيرات، حاول كتابة {{variable}}.',
|
||||
openingQuestion: 'أسئلة افتتاحية',
|
||||
openingQuestionPlaceholder: 'يمكنك استخدام المتغيرات، حاول كتابة {{variable}}.',
|
||||
noDataPlaceHolder:
|
||||
'يمكن أن يساعد بدء المحادثة مع المستخدم الذكاء الاصطناعي على إنشاء اتصال أوثق معهم في تطبيقات المحادثة.',
|
||||
varTip: 'يمكنك استخدام المتغيرات، حاول كتابة {{variable}}',
|
||||
tooShort: 'مطلوب ما لا يقل عن 20 كلمة من المطالبة الأولية لإنشاء ملاحظات افتتاحية للمحادثة.',
|
||||
notIncludeKey: 'لا تتضمن المطالبة الأولية المتغير: {{key}}. يرجى إضافته إلى المطالبة الأولية.',
|
||||
},
|
||||
modelConfig: {
|
||||
model: 'نموذج',
|
||||
setTone: 'تعيين نبرة الاستجابات',
|
||||
title: 'النموذج والمعلمات',
|
||||
modeType: {
|
||||
chat: 'دردشة',
|
||||
completion: 'إكمال',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
title: 'تصحيح ومعاينة',
|
||||
noPrompt: 'حاول كتابة بعض المطالبات في مدخلات ما قبل المطالبة',
|
||||
userInputField: 'حقل إدخال المستخدم',
|
||||
noVar: 'املأ قيمة المتغير، والتي سيتم استبدالها تلقائيًا في كلمة المطالبة في كل مرة يتم فيها بدء جلسة جديدة.',
|
||||
chatVarTip:
|
||||
'املأ قيمة المتغير، والتي سيتم استبدالها تلقائيًا في كلمة المطالبة في كل مرة يتم فيها بدء جلسة جديدة',
|
||||
completionVarTip:
|
||||
'املأ قيمة المتغير، والتي سيتم استبدالها تلقائيًا في كلمات المطالبة في كل مرة يتم فيها إرسال سؤال.',
|
||||
previewTitle: 'معاينة المطالبة',
|
||||
queryTitle: 'محتوى الاستعلام',
|
||||
queryPlaceholder: 'يرجى إدخال نص الطلب.',
|
||||
run: 'تشغيل',
|
||||
},
|
||||
result: 'نص الإخراج',
|
||||
noResult: 'سيتم عرض الإخراج هنا.',
|
||||
datasetConfig: {
|
||||
settingTitle: 'إعدادات الاسترجاع',
|
||||
knowledgeTip: 'انقر فوق الزر "+" لإضافة معرفة',
|
||||
retrieveOneWay: {
|
||||
title: 'استرجاع N-to-1',
|
||||
description: 'بناءً على نية المستخدم وأوصاف المعرفة، يختار الوكيل بشكل مستقل أفضل معرفة للاستعلام. الأفضل للتطبيقات ذات المعرفة المحددة والمحدودة.',
|
||||
},
|
||||
retrieveMultiWay: {
|
||||
title: 'استرجاع متعدد المسارات',
|
||||
description: 'بناءً على نية المستخدم، يستعلم عبر جميع المعارف، ويسترجع النص ذي الصلة من مصادر متعددة، ويختار أفضل النتائج المطابقة لاستعلام المستخدم بعد إعادة الترتيب.',
|
||||
},
|
||||
embeddingModelRequired: 'مطلوب نموذج تضمين مكون',
|
||||
rerankModelRequired: 'مطلوب نموذج إعادة ترتيب مكون',
|
||||
params: 'معلمات',
|
||||
top_k: 'أفضل K',
|
||||
top_kTip: 'يستخدم لتصفية القطع الأكثر تشابهًا مع أسئلة المستخدم. سيقوم النظام أيضًا بضبط قيمة Top K ديناميكيًا، وفقًا لـ max_tokens للنموذج المحدد.',
|
||||
score_threshold: 'عتبة النتيجة',
|
||||
score_thresholdTip: 'يستخدم لتعيين عتبة التشابه لتصفية القطع.',
|
||||
retrieveChangeTip: 'قد يؤثر تعديل وضع الفهرس ووضع الاسترجاع على التطبيقات المرتبطة بهذه المعرفة.',
|
||||
},
|
||||
debugAsSingleModel: 'تصحيح كنموذج واحد',
|
||||
debugAsMultipleModel: 'تصحيح كنماذج متعددة',
|
||||
duplicateModel: 'تكرار',
|
||||
publishAs: 'نشر كـ',
|
||||
assistantType: {
|
||||
name: 'نوع المساعد',
|
||||
chatAssistant: {
|
||||
name: 'مساعد أساسي',
|
||||
description: 'بناء مساعد قائم على الدردشة باستخدام نموذج لغة كبير',
|
||||
},
|
||||
agentAssistant: {
|
||||
name: 'مساعد وكيل',
|
||||
description: 'بناء وكيل ذكي يمكنه اختيار الأدوات بشكل مستقل لإكمال المهام',
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
agentMode: 'وضع الوكيل',
|
||||
agentModeDes: 'تعيين نوع وضع الاستدلال للوكيل',
|
||||
agentModeType: {
|
||||
ReACT: 'ReAct',
|
||||
functionCall: 'Function Calling',
|
||||
},
|
||||
setting: {
|
||||
name: 'إعدادات الوكيل',
|
||||
description: 'تسمح إعدادات مساعد الوكيل بتعيين وضع الوكيل والميزات المتقدمة مثل المطالبات المضمنة، المتاحة فقط في نوع الوكيل.',
|
||||
maximumIterations: {
|
||||
name: 'الحد الأقصى للتكرارات',
|
||||
description: 'الحد من عدد التكرارات التي يمكن لمساعد الوكيل تنفيذها',
|
||||
},
|
||||
},
|
||||
buildInPrompt: 'المطالبة المضمنة',
|
||||
firstPrompt: 'المطالبة الأولى',
|
||||
nextIteration: 'التكرار التالي',
|
||||
promptPlaceholder: 'اكتب مطالبتك هنا',
|
||||
tools: {
|
||||
name: 'الأدوات',
|
||||
description: 'يمكن أن يؤدي استخدام الأدوات إلى توسيع قدرات LLM، مثل البحث في الإنترنت أو إجراء العمليات الحسابية العلمية',
|
||||
enabled: 'ممكن',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
const translation = {
|
||||
title: 'السجلات',
|
||||
description: 'تسجل السجلات حالة تشغيل التطبيق، بما في ذلك مدخلات المستخدم واستجابات الذكاء الاصطناعي.',
|
||||
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
|
||||
table: {
|
||||
header: {
|
||||
time: 'الوقت',
|
||||
endUser: 'المستخدم النهائي',
|
||||
input: 'الإدخال',
|
||||
output: 'الإخراج',
|
||||
summary: 'العنوان',
|
||||
messageCount: 'عدد الرسائل',
|
||||
userRate: 'معدل المستخدم',
|
||||
adminRate: 'معدل المسؤول',
|
||||
startTime: 'وقت البدء',
|
||||
status: 'الحالة',
|
||||
runtime: 'وقت التشغيل',
|
||||
tokens: 'الرموز',
|
||||
user: 'المستخدم',
|
||||
version: 'الإصدار',
|
||||
updatedTime: 'الوقت المحدث',
|
||||
triggered_from: 'محفّز بواسطة',
|
||||
},
|
||||
pagination: {
|
||||
previous: 'السابق',
|
||||
next: 'التالي',
|
||||
},
|
||||
empty: {
|
||||
noChat: 'لا توجد محادثة حتى الآن',
|
||||
noOutput: 'لا توجد مخرجات',
|
||||
element: {
|
||||
title: 'هل هناك أي شخص؟',
|
||||
content: 'راقب وتهميش تفاعلات المستخدمين النهائيين والتطبيقات الذكية هنا لتحسين دقة الذكاء الاصطناعي باستمرار.',
|
||||
},
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
time: 'الوقت',
|
||||
conversationId: 'معرف المحادثة',
|
||||
promptTemplate: 'قالب المطالبة',
|
||||
promptTemplateBeforeChat: 'قالب المطالبة قبل الدردشة · كرسالة نظام',
|
||||
annotationTip: 'تحسينات تم وضع علامة عليها بواسطة {{user}}',
|
||||
timeConsuming: '',
|
||||
second: 'ثانية',
|
||||
tokenCost: 'تكلفة الرموز',
|
||||
loading: 'جاري التحميل',
|
||||
operation: {
|
||||
like: 'إعجاب',
|
||||
dislike: 'لم يعجبني',
|
||||
addAnnotation: 'إضافة تحسين',
|
||||
editAnnotation: 'تعديل التحسين',
|
||||
annotationPlaceholder: 'أدخل الإجابة المتوقعة التي تريد أن يرد بها الذكاء الاصطناعي، والتي يمكن استخدامها لضبط النموذج والتحسين المستمر لجودة توليد النص.',
|
||||
},
|
||||
variables: 'المتغيرات',
|
||||
uploadImages: 'الصور المحملة',
|
||||
modelParams: 'معلمات النموذج',
|
||||
},
|
||||
filter: {
|
||||
period: {
|
||||
today: 'اليوم',
|
||||
last7days: 'آخر 7 أيام',
|
||||
last4weeks: 'آخر 4 أسابيع',
|
||||
last3months: 'آخر 3 أشهر',
|
||||
last12months: 'آخر 12 شهرًا',
|
||||
monthToDate: 'الشهر حتى الآن',
|
||||
quarterToDate: 'الربع حتى الآن',
|
||||
yearToDate: 'السنة حتى الآن',
|
||||
allTime: 'كل الوقت',
|
||||
last30days: 'آخر 30 يومًا',
|
||||
custom: 'مخصص',
|
||||
},
|
||||
annotation: {
|
||||
all: 'الكل',
|
||||
annotated: 'تحسينات موصوفة ({{count}})',
|
||||
not_annotated: 'غير موصوفة',
|
||||
},
|
||||
sortBy: 'رتب حسب:',
|
||||
descending: 'تنازلي',
|
||||
ascending: 'تصاعدي',
|
||||
},
|
||||
workflowTitle: 'سجلات سير العمل',
|
||||
workflowSubtitle: 'سجل تفاصيل تشغيل سير العمل.',
|
||||
runDetail: {
|
||||
title: 'سجل المحادثة',
|
||||
workflowTitle: 'تفاصيل السجل',
|
||||
fileListLabel: 'تفاصيل الملف',
|
||||
fileListDetail: 'تفاصيل',
|
||||
testWithParams: 'اختبار مع المعلمات',
|
||||
},
|
||||
promptLog: 'سجل المطالبة',
|
||||
agentLog: 'سجل الوكيل',
|
||||
viewLog: 'عرض السجل',
|
||||
agentLogDetail: {
|
||||
agentMode: 'وضع الوكيل',
|
||||
toolUsed: 'الأداة المستخدمة',
|
||||
iterations: 'التكرارات',
|
||||
iteration: 'تكرار',
|
||||
finalProcessing: 'المعالجة النهائية',
|
||||
},
|
||||
triggerBy: {
|
||||
debugging: 'تصحيح الأخطاء',
|
||||
appRun: 'تشغيل التطبيق',
|
||||
webhook: 'Webhook',
|
||||
schedule: 'الجدول الزمني',
|
||||
plugin: 'المكون الإضافي',
|
||||
ragPipelineRun: 'تشغيل خط أنابيب RAG',
|
||||
ragPipelineDebugging: 'تصحيح أخطاء RAG',
|
||||
},
|
||||
dateFormat: 'شهر/يوم/سنة',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
const translation = {
|
||||
welcome: {
|
||||
firstStepTip: 'للبدء،',
|
||||
enterKeyTip: 'أدخل مفتاح OpenAI API الخاص بك أدناه',
|
||||
getKeyTip: 'احصل على مفتاح API الخاص بك من لوحة تحكم OpenAI',
|
||||
placeholder: 'مفتاح OpenAI API الخاص بك (مثلا sk-xxxx)',
|
||||
},
|
||||
apiKeyInfo: {
|
||||
cloud: {
|
||||
trial: {
|
||||
title: 'أنت تستخدم حصة تجربة {{providerName}}.',
|
||||
description: 'يتم توفير حصة التجربة لأغراض الاختبار الخاصة بك. قبل استنفاد حصة التجربة، يرجى إعداد مزود النموذج الخاص بك أو شراء حصة إضافية.',
|
||||
},
|
||||
exhausted: {
|
||||
title: 'تم استنفاد حصة التجربة الخاصة بك، يرجى إعداد مفتاح API الخاص بك.',
|
||||
description: 'لقد استنفدت حصة التجربة الخاصة بك. يرجى إعداد مزود النموذج الخاص بك أو شراء حصة إضافية.',
|
||||
},
|
||||
},
|
||||
selfHost: {
|
||||
title: {
|
||||
row1: 'للبدء،',
|
||||
row2: 'قم بإعداد مزود النموذج الخاص بك أولاً.',
|
||||
},
|
||||
},
|
||||
callTimes: 'أوقات الاتصال',
|
||||
usedToken: 'رمز مستخدم',
|
||||
setAPIBtn: 'الذهاب لإعداد مزود النموذج',
|
||||
tryCloud: 'أو جرب النسخة السحابية من Dify مع عرض مجاني',
|
||||
},
|
||||
overview: {
|
||||
title: 'نظرة عامة',
|
||||
appInfo: {
|
||||
title: 'تطبيق ويب',
|
||||
explanation: 'تطبيق ويب AI جاهز للاستخدام',
|
||||
accessibleAddress: 'عنوان URL عام',
|
||||
preview: 'معاينة',
|
||||
launch: 'إطلاق',
|
||||
regenerate: 'إعادة إنشاء',
|
||||
regenerateNotice: 'هل تريد إعادة إنشاء عنوان URL العام؟',
|
||||
preUseReminder: 'يرجى تمكين تطبيق الويب قبل المتابعة.',
|
||||
enableTooltip: {
|
||||
description: 'لتمكين هذه الميزة، يرجى إضافة عقدة إدخال المستخدم إلى اللوحة. (قد تكون موجودة بالفعل في المسودة، وتدخل حيز التنفيذ بعد النشر)',
|
||||
learnMore: 'اعرف المزيد',
|
||||
},
|
||||
settings: {
|
||||
entry: 'الإعدادات',
|
||||
title: 'إعدادات تطبيق الويب',
|
||||
modalTip: 'إعدادات تطبيق الويب من جانب العميل. ',
|
||||
webName: 'اسم تطبيق الويب',
|
||||
webDesc: 'وصف تطبيق الويب',
|
||||
webDescTip: 'سيتم عرض هذا النص على جانب العميل، مما يوفر إرشادات أساسية حول كيفية استخدام التطبيق',
|
||||
webDescPlaceholder: 'أدخل وصف تطبيق الويب',
|
||||
language: 'اللغة',
|
||||
workflow: {
|
||||
title: 'سير العمل',
|
||||
subTitle: 'تفاصيل سير العمل',
|
||||
show: 'عرض',
|
||||
hide: 'إخفاء',
|
||||
showDesc: 'عرض أو إخفاء تفاصيل سير العمل في تطبيق الويب',
|
||||
},
|
||||
chatColorTheme: 'سمة لون الدردشة',
|
||||
chatColorThemeDesc: 'تعيين سمة لون روبوت الدردشة',
|
||||
chatColorThemeInverted: 'معكوس',
|
||||
invalidHexMessage: 'قيمة hex غير صالحة',
|
||||
invalidPrivacyPolicy: 'رابط سياسة الخصوصية غير صالح. يرجى استخدام رابط صالح يبدأ بـ http أو https',
|
||||
sso: {
|
||||
label: 'فرض SSO',
|
||||
title: 'تطبيق ويب SSO',
|
||||
description: 'يُطلب من جميع المستخدمين تسجيل الدخول باستخدام SSO قبل استخدام تطبيق الويب',
|
||||
tooltip: 'اتصل بالمسؤول لتمكين تطبيق ويب SSO',
|
||||
},
|
||||
more: {
|
||||
entry: 'عرض المزيد من الإعدادات',
|
||||
copyright: 'حقوق النشر',
|
||||
copyrightTip: 'عرض معلومات حقوق النشر في تطبيق الويب',
|
||||
copyrightTooltip: 'يرجى الترقية إلى الخطة الاحترافية أو أعلى',
|
||||
copyRightPlaceholder: 'أدخل اسم المؤلف أو المنظمة',
|
||||
privacyPolicy: 'سياسة الخصوصية',
|
||||
privacyPolicyPlaceholder: 'أدخل رابط سياسة الخصوصية',
|
||||
privacyPolicyTip: 'يساعد الزوار على فهم البيانات التي يجمعها التطبيق، راجع <privacyPolicyLink>سياسة الخصوصية</privacyPolicyLink> لـ Dify.',
|
||||
customDisclaimer: 'إخلاء مسؤولية مخصص',
|
||||
customDisclaimerPlaceholder: 'أدخل نص إخلاء المسؤولية المخصص',
|
||||
customDisclaimerTip: 'سيتم عرض نص إخلاء المسؤولية المخصص على جانب العميل، مما يوفر معلومات إضافية حول التطبيق',
|
||||
},
|
||||
},
|
||||
embedded: {
|
||||
entry: 'مضمن',
|
||||
title: 'تضمين في الموقع',
|
||||
explanation: 'اختر طريقة لتضمين تطبيق الدردشة في موقعك',
|
||||
iframe: 'لإضافة تطبيق الدردشة في أي مكان على موقعك، أضف هذا iframe إلى كود html الخاص بك.',
|
||||
scripts: 'لإضافة تطبيق دردشة إلى أسفل يمين موقعك، أضف هذا الكود إلى html الخاص بك.',
|
||||
chromePlugin: 'تثبيت ملحق Dify Chatbot Chrome',
|
||||
copied: 'تم النسخ',
|
||||
copy: 'نسخ',
|
||||
},
|
||||
qrcode: {
|
||||
title: 'رمز الاستجابة السريعة للرابط',
|
||||
scan: 'مسح للمشاركة',
|
||||
download: 'تحميل رمز الاستجابة السريعة',
|
||||
},
|
||||
customize: {
|
||||
way: 'طريقة',
|
||||
entry: 'تخصيص',
|
||||
title: 'تخصيص تطبيق ويب AI',
|
||||
explanation: 'يمكنك تخصيص الواجهة الأمامية لتطبيق الويب لتناسب سيناريو واحتياجات أسلوبك.',
|
||||
way1: {
|
||||
name: 'انسخ كود العميل، وقم بتعديله وانشره على Vercel (موصى به)',
|
||||
step1: 'انسخ كود العميل وقم بتعديله',
|
||||
step1Tip: 'انقر هنا لنسخ الكود المصدري إلى حساب GitHub الخاص بك وتعديل الكود',
|
||||
step1Operation: 'Dify-WebClient',
|
||||
step2: 'نشر على Vercel',
|
||||
step2Tip: 'انقر هنا لاستيراد المستودع إلى Vercel والنشر',
|
||||
step2Operation: 'استيراد المستودع',
|
||||
step3: 'تكوين متغيرات البيئة',
|
||||
step3Tip: 'أضف متغيرات البيئة التالية في Vercel',
|
||||
},
|
||||
way2: {
|
||||
name: 'كتابة كود من جانب العميل لاستدعاء API ونشره على خادم',
|
||||
operation: 'التوثيق',
|
||||
},
|
||||
},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'واجهة برمجة تطبيقات خدمة الخلفية',
|
||||
explanation: 'سهلة الدمج في تطبيقك',
|
||||
accessibleAddress: 'نقطة نهاية واجهة برمجة تطبيقات الخدمة',
|
||||
doc: 'مرجع API',
|
||||
},
|
||||
triggerInfo: {
|
||||
title: 'المشغلات',
|
||||
explanation: 'إدارة مشغلات سير العمل',
|
||||
triggersAdded: 'تمت إضافة {{count}} مشغلات',
|
||||
noTriggerAdded: 'لم تتم إضافة أي مشغل',
|
||||
triggerStatusDescription: 'تظهر حالة عقدة المشغل هنا. (قد تكون موجودة بالفعل في المسودة، وتدخل حيز التنفيذ بعد النشر)',
|
||||
learnAboutTriggers: 'تعرف على المشغلات',
|
||||
},
|
||||
status: {
|
||||
running: 'في الخدمة',
|
||||
disable: 'تعطيل',
|
||||
},
|
||||
disableTooltip: {
|
||||
triggerMode: 'ميزة {{feature}} غير مدعومة في وضع عقدة المشغل.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'تحليل',
|
||||
ms: 'مللي ثانية',
|
||||
tokenPS: 'الرموز/ثانية',
|
||||
totalMessages: {
|
||||
title: 'إجمالي الرسائل',
|
||||
explanation: 'عدد تفاعلات الذكاء الاصطناعي اليومية؛ يمنع هندسة/تصحيح المطالبة.',
|
||||
},
|
||||
totalConversations: {
|
||||
title: 'إجمالي المحادثات',
|
||||
explanation: 'عدد المحادثات اليومية للذكاء الاصطناعي؛ باستثناء هندسة/تصحيح المطالبة.',
|
||||
},
|
||||
activeUsers: {
|
||||
title: 'المستخدمون النشطون',
|
||||
explanation: 'المستخدمون الفريدون الذين يشاركون في Q&A مع المساعد؛ يستبعد هندسة/تصحيح المطالبة.',
|
||||
},
|
||||
tokenUsage: {
|
||||
title: 'استخدام الرموز',
|
||||
explanation: 'يعكس استخدام الرموز اليومية لنموذج اللغة لتطبيق WebApp، مفيدًا للتحكم في التكلفة.',
|
||||
consumed: 'المستهلكة',
|
||||
},
|
||||
avgSessionInteractions: {
|
||||
title: 'متوسط تفاعلات الجلسة',
|
||||
explanation: 'عدد مفاتيح التواصل المستمر بين المستخدم والذكاء الاصطناعي؛ للمطبيقات القائمة على المحادثة.',
|
||||
},
|
||||
avgUserInteractions: {
|
||||
title: 'متوسط تفاعلات المستخدم',
|
||||
explanation: 'يعكس تكرار الاستخدام اليومي للمستخدمين. يعكس هذا المقياس لزوجة المستخدم.',
|
||||
},
|
||||
userSatisfactionRate: {
|
||||
title: 'معدل رضا المستخدم',
|
||||
explanation: 'عدد الإعجابات لكل 1000 رسالة. يشير هذا إلى النسبة التي يرضى فيها المستخدمون للغاية عن الإجابات.',
|
||||
},
|
||||
avgResponseTime: {
|
||||
title: 'متوسط وقت الاستجابة',
|
||||
explanation: 'الوقت (مللي ثانية) حتى يقوم الذكاء الاصطناعي بالمعالجة/الاستجابة؛ للمطبيقات النصية (text-based).',
|
||||
},
|
||||
tps: {
|
||||
title: 'سرعة إخراج الرمز',
|
||||
explanation: 'قياس أداء LLM. عد الرموز إخراج LLM من بداية الطلب إلى اكتمال الإخراج.',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
const translation = {
|
||||
createApp: 'إنشاء تطبيق',
|
||||
types: {
|
||||
all: 'الكل',
|
||||
chatbot: 'روبوت دردشة',
|
||||
agent: 'Agent',
|
||||
workflow: 'سير العمل (Workflow)',
|
||||
completion: 'إكمال',
|
||||
advanced: 'Chatflow',
|
||||
basic: 'أساسي',
|
||||
},
|
||||
duplicate: 'نسخ',
|
||||
mermaid: {
|
||||
handDrawn: 'رسم يدوي',
|
||||
classic: 'كلاسيكي',
|
||||
},
|
||||
duplicateTitle: 'نسخ التطبيق',
|
||||
export: 'تصدير DSL',
|
||||
exportFailed: 'فشل تصدير DSL.',
|
||||
importDSL: 'استيراد ملف DSL',
|
||||
createFromConfigFile: 'إنشاء من ملف DSL',
|
||||
importFromDSL: 'استيراد من DSL',
|
||||
importFromDSLFile: 'من ملف DSL',
|
||||
importFromDSLUrl: 'من رابط',
|
||||
importFromDSLUrlPlaceholder: 'لصق رابط DSL هنا',
|
||||
dslUploader: {
|
||||
button: 'اسحب وأفلت الملف، أو',
|
||||
browse: 'تصفح',
|
||||
},
|
||||
deleteAppConfirmTitle: 'حذف هذا التطبيق؟',
|
||||
deleteAppConfirmContent:
|
||||
'حذف التطبيق لا رجعة فيه. لن يتمكن المستخدمون من الوصول إلى تطبيقك بعد الآن، وسيتم حذف جميع تكوينات المطالبة والسجلات بشكل دائم.',
|
||||
appDeleted: 'تم حذف التطبيق',
|
||||
appDeleteFailed: 'فشل حذف التطبيق',
|
||||
join: 'انضم إلى المجتمع',
|
||||
communityIntro:
|
||||
'ناقش مع أعضاء الفريق والمساهمين والمطورين على قنوات مختلفة.',
|
||||
roadmap: 'شاهد خريطة الطريق',
|
||||
newApp: {
|
||||
learnMore: 'اعرف المزيد',
|
||||
startFromBlank: 'إنشاء من البداية',
|
||||
startFromTemplate: 'إنشاء من قالب',
|
||||
foundResult: '{{count}} نتيجة',
|
||||
foundResults: '{{count}} نتائج',
|
||||
noAppsFound: 'لم يتم العثور على تطبيقات',
|
||||
noTemplateFound: 'لم يتم العثور على قوالب',
|
||||
noTemplateFoundTip: 'حاول البحث باستخدام كلمات مفتاحية مختلفة.',
|
||||
chatbotShortDescription: 'روبوت دردشة قائم على LLM مع إعداد بسيط',
|
||||
chatbotUserDescription: 'قم ببناء روبوت دردشة قائم على LLM بسرعة مع تكوين بسيط. يمكنك التبديل إلى Chatflow لاحقًا.',
|
||||
completionShortDescription: 'مساعد AI لمهام توليد النصوص',
|
||||
completionUserDescription: 'قم ببناء مساعد AI لمهام توليد النصوص بسرعة مع تكوين بسيط.',
|
||||
agentShortDescription: 'وكيل ذكي مع الاستدلال واستخدام الأدوات المستقل',
|
||||
agentUserDescription: 'وكيل ذكي قادر على الاستدلال التكراري واستخدام الأدوات بشكل مستقل لتحقيق أهداف المهمة.',
|
||||
workflowShortDescription: 'تدفق وكيل للأتمتة الذكية',
|
||||
workflowUserDescription: 'قم ببناء تدفقات عمل AI مستقلة بشكل مرئي مع بساطة السحب والإفلات.',
|
||||
workflowWarning: 'حاليا في النسخة التجريبية (beta)',
|
||||
advancedShortDescription: 'سير عمل محسن للمحادثات متعددة الأدوار',
|
||||
advancedUserDescription: 'سير عمل مع ميزات ذاكرة إضافية وواجهة روبوت دردشة.',
|
||||
chooseAppType: 'اختر نوع التطبيق',
|
||||
forBeginners: 'أنواع تطبيقات أبسط',
|
||||
forAdvanced: 'للمستخدمين المتقدمين',
|
||||
noIdeaTip: 'لا توجد أفكار؟ تحقق من قوالبنا',
|
||||
captionName: 'اسم التطبيق والأيقونة',
|
||||
appNamePlaceholder: 'أعط اسمًا لتطبيقك',
|
||||
captionDescription: 'الوصف',
|
||||
optional: 'اختياري',
|
||||
appDescriptionPlaceholder: 'أدخل وصف التطبيق',
|
||||
useTemplate: 'استخدم هذا القالب',
|
||||
previewDemo: 'معاينة العرض التوضيحي',
|
||||
chatApp: 'مساعد',
|
||||
chatAppIntro:
|
||||
'أريد بناء تطبيق قائم على الدردشة. يستخدم هذا التطبيق تنسيق سؤال وجواب، مما يسمح بجولات متعددة من المحادثة المستمرة.',
|
||||
agentAssistant: 'مساعد وكيل جديد',
|
||||
completeApp: 'مولد نصوص',
|
||||
completeAppIntro:
|
||||
'أريد إنشاء تطبيق يولد نصوصًا عالية الجودة بناءً على المطالبات، مثل إنشاء المقالات والملخصات والترجمات والمزيد.',
|
||||
showTemplates: 'أريد الاختيار من قالب',
|
||||
hideTemplates: 'العودة إلى اختيار الوضع',
|
||||
Create: 'إنشاء',
|
||||
Cancel: 'إلغاء',
|
||||
Confirm: 'تأكيد',
|
||||
import: 'استيراد',
|
||||
nameNotEmpty: 'لا يمكن أن يكون الاسم فارغًا',
|
||||
appTemplateNotSelected: 'الرجاء تحديد قالب',
|
||||
appTypeRequired: 'الرجاء تحديد نوع التطبيق',
|
||||
appCreated: 'تم إنشاء التطبيق',
|
||||
caution: 'تحذير',
|
||||
appCreateDSLWarning: 'تحذير: قد يؤثر اختلاف إصدار DSL على ميزات معينة',
|
||||
appCreateDSLErrorTitle: 'عدم توافق الإصدار',
|
||||
appCreateDSLErrorPart1: 'تم اكتشاف اختلاف كبير في إصدارات DSL. قد يؤدي فرض الاستيراد إلى تعطل التطبيق.',
|
||||
appCreateDSLErrorPart2: 'هل تريد المتابعة؟',
|
||||
appCreateDSLErrorPart3: 'إصدار DSL للتطبيق الحالي: ',
|
||||
appCreateDSLErrorPart4: 'إصدار DSL المدعوم من النظام: ',
|
||||
appCreateFailed: 'فشل إنشاء التطبيق',
|
||||
dropDSLToCreateApp: 'أفلت ملف DSL هنا لإنشاء تطبيق',
|
||||
},
|
||||
newAppFromTemplate: {
|
||||
byCategories: 'حسب الفئات',
|
||||
searchAllTemplate: 'بحث في كل القوالب...',
|
||||
sidebar: {
|
||||
Recommended: 'موصى به',
|
||||
Agent: 'Agent',
|
||||
Assistant: 'مساعد',
|
||||
HR: 'الموارد البشرية',
|
||||
Workflow: 'سير العمل',
|
||||
Writing: 'كتابة',
|
||||
Programming: 'برمجة',
|
||||
},
|
||||
},
|
||||
editApp: 'تعديل المعلومات',
|
||||
editAppTitle: 'تعديل معلومات التطبيق',
|
||||
editDone: 'تم تحديث معلومات التطبيق',
|
||||
editFailed: 'فشل تحديث معلومات التطبيق',
|
||||
iconPicker: {
|
||||
ok: 'موافق',
|
||||
cancel: 'إلغاء',
|
||||
emoji: 'رموز تعبيرية',
|
||||
image: 'صورة',
|
||||
},
|
||||
answerIcon: {
|
||||
title: 'استخدم أيقونة تطبيق الويب لاستبدال 🤖',
|
||||
description: 'ما إذا كان سيتم استخدام أيقونة تطبيق الويب لاستبدال 🤖 في التطبيق المشترك',
|
||||
descriptionInExplore: 'ما إذا كان سيتم استخدام أيقونة تطبيق الويب لاستبدال 🤖 في الاستكشاف',
|
||||
},
|
||||
switch: 'التبديل إلى Workflow Orchestrate',
|
||||
switchTipStart: 'سيتم إنشاء نسخة تطبيق جديدة لك، وستنتقل النسخة الجديدة إلى Workflow Orchestrate. النسخة الجديدة ستكون ',
|
||||
switchTip: 'غير مسموح',
|
||||
switchTipEnd: ' بالعودة إلى Basic Orchestrate.',
|
||||
switchLabel: 'نسخة التطبيق التي سيتم إنشاؤها',
|
||||
removeOriginal: 'حذف التطبيق الأصلي',
|
||||
switchStart: 'بدء التبديل',
|
||||
openInExplore: 'فتح في الاستكشاف',
|
||||
typeSelector: {
|
||||
all: 'كل الأنواع',
|
||||
chatbot: 'روبوت دردشة',
|
||||
agent: 'Agent',
|
||||
workflow: 'سير العمل',
|
||||
completion: 'إكمال',
|
||||
advanced: 'Chatflow',
|
||||
},
|
||||
tracing: {
|
||||
title: 'تتبع أداء التطبيق',
|
||||
description: 'تكوين مزود LLMOps خارجي وتتبع أداء التطبيق.',
|
||||
config: 'تكوين',
|
||||
view: 'عرض',
|
||||
collapse: 'طي',
|
||||
expand: 'توسيع',
|
||||
tracing: 'تتبع',
|
||||
disabled: 'معطل',
|
||||
disabledTip: 'الرجاء تكوين المزود أولاً',
|
||||
enabled: 'في الخدمة',
|
||||
tracingDescription: 'التقاط السياق الكامل لتنفيذ التطبيق، بما في ذلك مكالمات LLM، والسياق، والمطالبات، وطلبات HTTP، والمزيد، إلى منصة تتبع تابعة لجهة خارجية.',
|
||||
configProviderTitle: {
|
||||
configured: 'تم التكوين',
|
||||
notConfigured: 'تكوين المزود لتمكين التتبع',
|
||||
moreProvider: 'مزيد من المزودين',
|
||||
},
|
||||
arize: {
|
||||
title: 'Arize',
|
||||
description: 'مراقبة LLM على مستوى المؤسسة، والتقييم عبر الإنترنت وغير المتصل بالإنترنت، والمراقبة، والتجريب - بدعم من OpenTelemetry. مصمم خصيصًا لتطبيقات LLM والتطبيقات التي تعتمد على الوكيل.',
|
||||
},
|
||||
phoenix: {
|
||||
title: 'Phoenix',
|
||||
description: 'منصة مفتوحة المصدر تعتمد على OpenTelemetry للمراقبة والتقييم وهندسة المطالبات والتجريب لتدفقات عمل LLM والوكلاء.',
|
||||
},
|
||||
langsmith: {
|
||||
title: 'LangSmith',
|
||||
description: 'منصة مطور شاملة لكل خطوة من خطوات دورة حياة التطبيق المدعوم بـ LLM.',
|
||||
},
|
||||
langfuse: {
|
||||
title: 'Langfuse',
|
||||
description: 'مراقبة LLM مفتوحة المصدر وتقييمها وإدارة المطالبات والمقاييس لتصحيح وتحسين تطبيق LLM الخاص بك.',
|
||||
},
|
||||
opik: {
|
||||
title: 'Opik',
|
||||
description: 'Opik هي منصة مفتوحة المصدر لتقييم واختبار ومراقبة تطبيقات LLM.',
|
||||
},
|
||||
weave: {
|
||||
title: 'Weave',
|
||||
description: 'Weave هي منصة مفتوحة المصدر لتقييم واختبار ومراقبة تطبيقات LLM.',
|
||||
},
|
||||
aliyun: {
|
||||
title: 'Cloud Monitor',
|
||||
description: 'منصة المراقبة المدارة بالكامل والتي لا تحتاج إلى صيانة والمقدمة من Alibaba Cloud، تتيح المراقبة الجاهزة والتتبع وتقييم تطبيقات Dify.',
|
||||
},
|
||||
mlflow: {
|
||||
title: 'MLflow',
|
||||
description: 'MLflow هي منصة مفتوحة المصدر لإدارة التجارب وتقييم ومراقبة تطبيقات LLM.',
|
||||
},
|
||||
databricks: {
|
||||
title: 'Databricks',
|
||||
description: 'توفر Databricks تدفق MLflow مدار بالكامل مع حوكمة وأمان قويين لتخزين بيانات التتبع.',
|
||||
},
|
||||
tencent: {
|
||||
title: 'Tencent APM',
|
||||
description: 'تُوفر مراقبة أداء التطبيقات من Tencent تتبعًا شاملاً وتحليلاً متعدد الأبعاد لتطبيقات LLM.',
|
||||
},
|
||||
inUse: 'قيد الاستخدام',
|
||||
configProvider: {
|
||||
title: 'تكوين ',
|
||||
placeholder: 'أدخل {{key}} الخاص بك',
|
||||
project: 'مشروع',
|
||||
trackingUri: 'رابط التتبع',
|
||||
experimentId: 'معرف التجربة',
|
||||
username: 'اسم المستخدم',
|
||||
password: 'كلمة المرور',
|
||||
publicKey: 'المفتاح العام',
|
||||
secretKey: 'المفتاح السري',
|
||||
viewDocsLink: 'عرض وثائق {{key}}',
|
||||
removeConfirmTitle: 'إزالة تكوين {{key}}؟',
|
||||
removeConfirmContent: 'التكوين الحالي قيد الاستخدام، وستؤدي إزالته إلى إيقاف ميزة التتبع.',
|
||||
clientId: 'معرف العميل (Client ID)',
|
||||
clientSecret: 'سر العميل (Client Secret)',
|
||||
personalAccessToken: 'رمز الوصول الشخصي (القديم)',
|
||||
databricksHost: 'عنوان URL لمساحة عمل Databricks',
|
||||
},
|
||||
},
|
||||
appSelector: {
|
||||
label: 'تطبيق',
|
||||
placeholder: 'اختر تطبيقًا...',
|
||||
params: 'معلمات التطبيق',
|
||||
noParams: 'لا توجد معلمات مطلوبة',
|
||||
},
|
||||
showMyCreatedAppsOnly: 'تم إنشاؤه بواسطتي',
|
||||
structOutput: {
|
||||
moreFillTip: 'يظهر 10 مستويات كحد أقصى من التداخل',
|
||||
required: 'مطلوب',
|
||||
LLMResponse: 'استجابة LLM',
|
||||
configure: 'تكوين',
|
||||
notConfiguredTip: 'لم يتم تكوين الإخراج الهيكلي بعد',
|
||||
structured: 'هيكلي',
|
||||
structuredTip: 'المخرجات الهيكلية هي ميزة تضمن أن يولد النموذج دائمًا استجابات تلتزم بـ JSON Schema الذي قدمته',
|
||||
modelNotSupported: 'النموذج غير مدعوم',
|
||||
modelNotSupportedTip: 'النموذج الحالي لا يدعم هذه الميزة ويتم تخفيضه تلقائيًا إلى حقن المطالبة.',
|
||||
},
|
||||
accessControl: 'التحكم في الوصول إلى تطبيق الويب',
|
||||
accessItemsDescription: {
|
||||
anyone: 'يمكن لأي شخص الوصول إلى تطبيق الويب (لا يلزم تسجيل الدخول)',
|
||||
specific: 'يمكن فقط لأعضاء محددين داخل المنصة الوصول إلى تطبيق الويب',
|
||||
organization: 'يمكن لجميع الأعضاء داخل المنصة الوصول إلى تطبيق الويب',
|
||||
external: 'يمكن فقط للمستخدمين الخارجيين authenticated الوصول إلى تطبيق الويب',
|
||||
},
|
||||
accessControlDialog: {
|
||||
title: 'التحكم في الوصول إلى تطبيق الويب',
|
||||
description: 'تعيين أذونات الوصول إلى تطبيق الويب',
|
||||
accessLabel: 'من لديه حق الوصول',
|
||||
accessItems: {
|
||||
anyone: 'أي شخص لديه الرابط',
|
||||
specific: 'أعضاء محددون داخل المنصة',
|
||||
organization: 'جميع الأعضاء داخل المنصة',
|
||||
external: 'المستخدمون الخارجيون Authenticated',
|
||||
},
|
||||
groups_one: '{{count}} مجموعة',
|
||||
groups_other: '{{count}} مجموعات',
|
||||
members_one: '{{count}} عضو',
|
||||
members_other: '{{count}} أعضاء',
|
||||
noGroupsOrMembers: 'لم يتم تحديد مجموعات أو أعضاء',
|
||||
webAppSSONotEnabledTip: 'الرجاء الاتصال بمسؤول المؤسسة لتكوين المصادقة الخارجية لتطبيق الويب.',
|
||||
operateGroupAndMember: {
|
||||
searchPlaceholder: 'بحث عن مجموعات وأعضاء',
|
||||
allMembers: 'جميع الأعضاء',
|
||||
expand: 'توسيع',
|
||||
noResult: 'لا توجد نتائج',
|
||||
},
|
||||
updateSuccess: 'تم التحديث بنجاح',
|
||||
},
|
||||
publishApp: {
|
||||
title: 'من يمكنه الوصول إلى تطبيق الويب',
|
||||
notSet: 'لم يتم تعيينه',
|
||||
notSetDesc: 'حاليا لا يمكن لأحد الوصول إلى تطبيق الويب. الرجاء تعيين الأذونات.',
|
||||
},
|
||||
noAccessPermission: 'لا يوجد إذن للوصول إلى تطبيق الويب',
|
||||
noUserInputNode: 'عقدة إدخال المستخدم مفقودة',
|
||||
notPublishedYet: 'التطبيق لم ينشر بعد',
|
||||
maxActiveRequests: 'أقصى عدد للطلبات المتزامنة',
|
||||
maxActiveRequestsPlaceholder: 'أدخل 0 لغير محدود',
|
||||
maxActiveRequestsTip: 'الحد الأقصى لعدد الطلبات النشطة المتزامنة لكل تطبيق (0 لغير محدود)',
|
||||
gotoAnything: {
|
||||
searchPlaceholder: 'ابحث أو اكتب @ أو / للأوامر...',
|
||||
searchTitle: 'ابحث عن أي شيء',
|
||||
searching: 'جاري البحث...',
|
||||
noResults: 'لم يتم العثور على نتائج',
|
||||
searchFailed: 'فشل البحث',
|
||||
searchTemporarilyUnavailable: 'البحث غير متاح مؤقتًا',
|
||||
servicesUnavailableMessage: 'قد تواجه بعض خدمات البحث مشكلات. حاول مرة أخرى لاحقًا.',
|
||||
someServicesUnavailable: 'بعض خدمات البحث غير متوفرة',
|
||||
resultCount: '{{count}} نتيجة',
|
||||
resultCount_other: '{{count}} نتائج',
|
||||
inScope: 'في {{scope}}',
|
||||
clearToSearchAll: 'امسح @ للبحث في الكل',
|
||||
useAtForSpecific: 'استخدم @ لأنواع محددة',
|
||||
selectToNavigate: 'اختر للانتقال',
|
||||
startTyping: 'ابدأ الكتابة للبحث',
|
||||
tips: 'اضغط ↑↓ للتنقل',
|
||||
pressEscToClose: 'اضغط ESC للإغلاق',
|
||||
selectSearchType: 'اختر ما تريد البحث عنه',
|
||||
searchHint: 'ابدأ الكتابة للبحث عن كل شيء على الفور',
|
||||
commandHint: 'اكتب @ للتصفح حسب الفئة',
|
||||
slashHint: 'اكتب / لرؤية جميع الأوامر المتاحة',
|
||||
actions: {
|
||||
searchApplications: 'بحث في التطبيقات',
|
||||
searchApplicationsDesc: 'البحث والانتقال إلى تطبيقاتك',
|
||||
searchPlugins: 'بحث في الإضافات',
|
||||
searchPluginsDesc: 'البحث والانتقال إلى إضافاتك',
|
||||
searchKnowledgeBases: 'بحث في قواعد المعرفة',
|
||||
searchKnowledgeBasesDesc: 'البحث والانتقال إلى قواعد المعرفة الخاصة بك',
|
||||
searchWorkflowNodes: 'بحث في عقد سير العمل',
|
||||
searchWorkflowNodesDesc: 'البحث والانتقال إلى العقد في سير العمل الحالي بالاسم أو النوع',
|
||||
searchWorkflowNodesHelp: 'هذه الميزة تعمل فقط عند عرض سير العمل. انتقل إلى سير العمل أولاً.',
|
||||
runTitle: 'أوامر',
|
||||
runDesc: 'تشغيل أوامر سريعة (السمة، اللغة، ...)',
|
||||
themeCategoryTitle: 'السمة',
|
||||
themeCategoryDesc: 'تبديل سمة التطبيق',
|
||||
themeSystem: 'سمة النظام',
|
||||
themeSystemDesc: 'اتبع مظهر نظام التشغيل',
|
||||
themeLight: 'السمة الفاتحة',
|
||||
themeLightDesc: 'استخدم المظهر الفاتح',
|
||||
themeDark: 'السمة الداكنة',
|
||||
themeDarkDesc: 'استخدم المظهر الداكن',
|
||||
languageCategoryTitle: 'اللغة',
|
||||
languageCategoryDesc: 'تبديل لغة الواجهة',
|
||||
languageChangeDesc: 'تغيير لغة واجهة المستخدم',
|
||||
slashDesc: 'تنفيذ الأوامر (اكتب / لرؤية جميع الأوامر المتاحة)',
|
||||
accountDesc: 'الانتقال إلى صفحة الحساب',
|
||||
communityDesc: 'فتح مجتمع Discord',
|
||||
docDesc: 'فتح وثائق المساعدة',
|
||||
feedbackDesc: 'فتح مناقشات ملاحظات المجتمع',
|
||||
zenTitle: 'وضع Zen',
|
||||
zenDesc: 'تبديل وضع التركيز على اللوحة',
|
||||
},
|
||||
emptyState: {
|
||||
noAppsFound: 'لم يتم العثور على تطبيقات',
|
||||
noPluginsFound: 'لم يتم العثور على إضافات',
|
||||
noKnowledgeBasesFound: 'لم يتم العثور على قواعد معرفة',
|
||||
noWorkflowNodesFound: 'لم يتم العثور على عقد سير عمل',
|
||||
tryDifferentTerm: 'جرب مصطلح بحث مختلف',
|
||||
trySpecificSearch: 'جرب {{shortcuts}} لعمليات بحث محددة',
|
||||
},
|
||||
groups: {
|
||||
apps: 'تطبيقات',
|
||||
plugins: 'إضافات',
|
||||
knowledgeBases: 'قواعد المعرفة',
|
||||
workflowNodes: 'عقد سير العمل',
|
||||
commands: 'أوامر',
|
||||
},
|
||||
noMatchingCommands: 'لم يتم العثور على أوامر مطابقة',
|
||||
tryDifferentSearch: 'جرب مصطلح بحث مختلف',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
const translation = {
|
||||
currentPlan: 'الخطة الحالية',
|
||||
usagePage: {
|
||||
teamMembers: 'أعضاء الفريق',
|
||||
buildApps: 'بناء التطبيقات',
|
||||
annotationQuota: 'حصة التعليقات التوضيحية',
|
||||
documentsUploadQuota: 'حصة رفع المستندات',
|
||||
vectorSpace: 'تخزين بيانات المعرفة',
|
||||
vectorSpaceTooltip: 'ستستهلك المستندات ذات وضع الفهرسة عالي الجودة موارد تخزين بيانات المعرفة. عندما يصل تخزين بيانات المعرفة إلى الحد الأقصى، لن يتم تحميل مستندات جديدة.',
|
||||
triggerEvents: 'أحداث المشغل',
|
||||
perMonth: 'شهريًا',
|
||||
resetsIn: 'يتم إعادة التعيين في {{count,number}} أيام',
|
||||
},
|
||||
teamMembers: 'أعضاء الفريق',
|
||||
triggerLimitModal: {
|
||||
title: 'ترقية لفتح المزيد من أحداث المشغل',
|
||||
description: 'لقد وصلت إلى الحد الأقصى لمشغلات أحداث سير العمل لهذه الخطة.',
|
||||
dismiss: 'تجاهل',
|
||||
upgrade: 'ترقية',
|
||||
usageTitle: 'أحداث المشغل',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: 'عرض الخطة',
|
||||
encourage: 'الترقية الآن',
|
||||
encourageShort: 'ترقية',
|
||||
},
|
||||
viewBilling: 'إدارة الفواتير والاشتراكات',
|
||||
buyPermissionDeniedTip: 'يرجى الاتصال بمسؤول المؤسسة للاشتراك',
|
||||
plansCommon: {
|
||||
title: {
|
||||
plans: 'الخطط',
|
||||
description: 'اختر الخطة التي تناسب احتياجات فريقك.',
|
||||
},
|
||||
freeTrialTipPrefix: 'سجل واحصل على ',
|
||||
freeTrialTip: 'تجربة مجانية لـ 200 مكالمة OpenAI. ',
|
||||
freeTrialTipSuffix: 'لا تتطلب بطاقة ائتمان',
|
||||
yearlyTip: 'ادفع لمدة 10 أشهر، واستمتع بسنة كاملة!',
|
||||
mostPopular: 'الأكثر شعبية',
|
||||
cloud: 'خدمة سحابية',
|
||||
self: 'مستضافة ذاتيًا',
|
||||
planRange: {
|
||||
monthly: 'شهري',
|
||||
yearly: 'سنوي',
|
||||
},
|
||||
month: 'شهر',
|
||||
year: 'سنة',
|
||||
save: 'وفر ',
|
||||
free: 'مجاني',
|
||||
annualBilling: 'الفوترة السنوية توفر {{percent}}%',
|
||||
taxTip: 'جميع أسعار الاشتراك (الشهرية / السنوية) لا تشمل الضرائب المطبقة (مثل ضريبة القيمة المضافة وضريبة المبيعات).',
|
||||
taxTipSecond: 'إذا لم تكن في منطقتك متطلبات ضريبية، فلن تظهر أي ضريبة عند الدفع، ولن يتم تحصيل أي رسوم إضافية طوال فترة الاشتراك.',
|
||||
comparePlanAndFeatures: 'قارن الخطط والميزات',
|
||||
priceTip: 'لكل مساحة عمل/',
|
||||
currentPlan: 'الخطة الحالية',
|
||||
contractSales: 'اتصل بالمبيعات',
|
||||
contractOwner: 'اتصل بمدير الفريق',
|
||||
startForFree: 'ابدأ مجانًا',
|
||||
startBuilding: 'ابدأ البناء',
|
||||
getStarted: 'ابدأ الآن',
|
||||
contactSales: 'اتصل بالمبيعات',
|
||||
talkToSales: 'تحدث إلى المبيعات',
|
||||
modelProviders: 'دعم OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replicate',
|
||||
teamWorkspace: '{{count,number}} مساحة عمل للفريق',
|
||||
teamMember_one: '{{count,number}} عضو في الفريق',
|
||||
teamMember_other: '{{count,number}} أعضاء في الفريق',
|
||||
annotationQuota: 'حصة التعليقات التوضيحية',
|
||||
buildApps: '{{count,number}} تطبيقات',
|
||||
documents: '{{count,number}} مستندات معرفة',
|
||||
documentsTooltip: 'الحصة لعدد المستندات المستوردة من مصدر بيانات المعرفة.',
|
||||
vectorSpace: '{{size}} تخزين بيانات المعرفة',
|
||||
vectorSpaceTooltip: 'ستستهلك المستندات ذات وضع الفهرسة عالي الجودة موارد تخزين بيانات المعرفة. عندما يصل تخزين بيانات المعرفة إلى الحد الأقصى، لن يتم تحميل مستندات جديدة.',
|
||||
documentsRequestQuota: '{{count,number}} طلب معرفة/دقيقة',
|
||||
documentsRequestQuotaTooltip: 'يحدد العدد الإجمالي للإجراءات التي يمكن لمساحة العمل تنفيذها كل دقيقة داخل قاعدة المعرفة، بما في ذلك إنشاء مجموعة البيانات، والحذف، والتحديثات، ورفع المستندات، والتعديلات، والأرشفة، واستعلامات قاعدة المعرفة. يتم استخدام هذا المقياس لتقييم أداء طلبات قاعدة المعرفة. على سبيل المثال، إذا أجرى مستخدم Sandbox 10 اختبارات hit متتالية في دقيقة واحدة، فسيتم تقييد مساحة العمل الخاصة به مؤقتًا من تنفيذ الإجراءات التالية للدقيقة التالية: إنشاء مجموعة البيانات، والحذف، والتحديثات، ورفع المستندات أو التعديلات. ',
|
||||
apiRateLimit: 'حد معدل API',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
unlimitedApiRate: 'لا يوجد حد لمعدل API لـ Dify',
|
||||
apiRateLimitTooltip: 'ينطبق حد معدل API على جميع الطلبات التي يتم إجراؤها من خلال Dify API، بما في ذلك توليد النصوص، ومحادثات الدردشة، وتنفيذ سير العمل، ومعالجة المستندات.',
|
||||
documentProcessingPriority: ' أولوية معالجة المستندات',
|
||||
documentProcessingPriorityTip: 'لأولوية معالجة مستندات أعلى، يرجى ترقية خطتك.',
|
||||
documentProcessingPriorityUpgrade: 'معالجة المزيد من البيانات بدقة أعلى وسرعة أكبر.',
|
||||
priority: {
|
||||
'standard': 'قياسي',
|
||||
'priority': 'أولوية',
|
||||
'top-priority': 'أولوية قصوى',
|
||||
},
|
||||
triggerEvents: {
|
||||
sandbox: '{{count,number}} أحداث مشغل',
|
||||
professional: '{{count,number}} أحداث مشغل/شهر',
|
||||
unlimited: 'أحداث مشغل غير محدودة',
|
||||
tooltip: 'عدد الأحداث التي تبدأ سير العمل تلقائيًا من خلال مشغلات الإضافات أو الجدول الزمني أو Webhook.',
|
||||
},
|
||||
workflowExecution: {
|
||||
standard: 'تنفيذ سير عمل قياسي',
|
||||
faster: 'تنفيذ سير عمل أسرع',
|
||||
priority: 'تنفيذ سير عمل ذو أولوية',
|
||||
tooltip: 'أولوية وسرعة قائمة انتظار تنفيذ سير العمل.',
|
||||
},
|
||||
startNodes: {
|
||||
limited: 'ما يصل إلى {{count}} مشغلات/سير عمل',
|
||||
unlimited: 'مشغلات غير محدودة/سير عمل',
|
||||
},
|
||||
logsHistory: '{{days}} تاريخ السجلات',
|
||||
customTools: 'أدوات مخصصة',
|
||||
unavailable: 'غير متوفر',
|
||||
days: 'أيام',
|
||||
unlimited: 'غير محدود',
|
||||
support: 'الدعم',
|
||||
supportItems: {
|
||||
communityForums: 'منتديات المجتمع',
|
||||
emailSupport: 'دعم البريد الإلكتروني',
|
||||
priorityEmail: 'أولوية دعم البريد الإلكتروني والدردشة',
|
||||
logoChange: 'تغيير الشعار',
|
||||
SSOAuthentication: 'مصادقة SSO',
|
||||
personalizedSupport: 'دعم مخصص',
|
||||
dedicatedAPISupport: 'دعم API مخصص',
|
||||
customIntegration: 'تكامل ودعم مخصص',
|
||||
ragAPIRequest: 'طلبات RAG API',
|
||||
bulkUpload: 'رفع المستندات بالجملة',
|
||||
agentMode: 'وضع الوكيل',
|
||||
workflow: 'سير العمل',
|
||||
llmLoadingBalancing: 'موازنة حمل LLM',
|
||||
llmLoadingBalancingTooltip: 'أضف مفاتيح API متعددة للنماذج، مما يتيح تجاوز حدود معدل API بشكل فعال. ',
|
||||
},
|
||||
comingSoon: 'قريبا',
|
||||
member: 'عضو',
|
||||
memberAfter: 'عضو',
|
||||
messageRequest: {
|
||||
title: '{{count,number}} أرصدة الرسائل',
|
||||
titlePerMonth: '{{count,number}} أرصدة رسائل/شهر',
|
||||
tooltip: 'يتم توفير أرصدة الرسائل لمساعدتك على تجربة نماذج OpenAI المختلفة بسهولة في Dify. يتم استهلاك الأرصدة بناءً على نوع النموذج. بمجرد نفادها، يمكنك التبديل إلى مفتاح OpenAI API الخاص بك.',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: '{{count,number}} حدود حصة التعليقات التوضيحية',
|
||||
tooltip: 'يوفر التحرير اليدوي والتعليق على الردود قدرات إجابة على الأسئلة عالية الجودة وقابلة للتخصيص للتطبيقات. (ينطبق فقط في تطبيقات الدردشة)',
|
||||
},
|
||||
ragAPIRequestTooltip: 'يشير إلى عدد مكالمات API التي تستدعي فقط قدرات معالجة قاعدة المعرفة في Dify.',
|
||||
receiptInfo: 'يمكن لمالك الفريق ومشرف الفريق فقط الاشتراك وعرض معلومات الفوترة',
|
||||
},
|
||||
plans: {
|
||||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
for: 'تجربة مجانية للقدرات الأساسية',
|
||||
description: 'جرب الميزات الأساسية مجانًا.',
|
||||
},
|
||||
professional: {
|
||||
name: 'احترافي',
|
||||
for: 'للمطورين المستقلين / الفرق الصغيرة',
|
||||
description: 'للمطورين المستقلين والفرق الصغيرة المستعدة لبناء تطبيقات الذكاء الاصطناعي الإنتاجية.',
|
||||
},
|
||||
team: {
|
||||
name: 'فريق',
|
||||
for: 'للفرق متوسطة الحجم',
|
||||
description: 'للفرق متوسطة الحجم التي تتطلب التعاون وإنتاجية أعلى.',
|
||||
},
|
||||
community: {
|
||||
name: 'مجتمع',
|
||||
for: 'للمستخدمين الأفراد، أو الفرق الصغيرة، أو المشاريع غير التجارية',
|
||||
description: 'للمتحمسين للمصادر المفتوحة، والمطورين الأفراد، والمشاريع غير التجارية',
|
||||
price: 'مجاني',
|
||||
btnText: 'ابدأ الآن',
|
||||
includesTitle: 'ميزات مجانية:',
|
||||
features: ['تم إصدار جميع الميزات الأساسية تحت المستودع العام', 'مساحة عمل واحدة', 'متوافق مع ترخيص ديفي المفتوح المصدر'],
|
||||
},
|
||||
premium: {
|
||||
name: 'بريميوم',
|
||||
for: 'للمؤسسات والفرق متوسطة الحجم',
|
||||
description: 'للمؤسسات متوسطة الحجم التي تحتاج إلى مرونة في النشر ودعم معزز',
|
||||
price: 'قابل للتطوير',
|
||||
priceTip: 'استنادًا إلى سوق السحابة',
|
||||
btnText: 'احصل على بريميوم على',
|
||||
includesTitle: 'كل شيء من المجتمع، بالإضافة إلى:',
|
||||
comingSoon: 'دعم Microsoft Azure و Google Cloud قريبا',
|
||||
features: ['الاعتمادية المدارة ذاتيًا من قبل مختلف مزودي السحابة', 'مساحة عمل واحدة', 'تخصيص شعار وهوية التطبيق الإلكتروني', 'دعم البريد الإلكتروني والمحادثة ذو الأولوية'],
|
||||
},
|
||||
enterprise: {
|
||||
name: 'مؤسسة (Enterprise)',
|
||||
for: 'للفرق كبيرة الحجم',
|
||||
description: 'للمؤسسات التي تتطلب أمانًا وامتثالًا وقابلية للتوسع وتحكمًا وحلولًا مخصصة على مستوى المؤسسة',
|
||||
price: 'مخصص',
|
||||
priceTip: 'الفوترة السنوية فقط',
|
||||
btnText: 'اتصل بالمبيعات',
|
||||
includesTitle: 'كل شيء من <highlight>بريميوم</highlight>، بالإضافة إلى:',
|
||||
features: ['حلول نشر قابلة للتوسع على مستوى المؤسسات', 'تفويض الترخيص التجاري', 'ميزات حصرية للمؤسسات', 'مساحات عمل متعددة وإدارة المؤسسات', 'تسجيل الدخول الموحد', 'اتفاقيات مستوى الخدمة المتفاوض عليها من قبل شركاء ديفي', 'الأمان والتحكم المتقدم', 'التحديثات والصيانة بواسطة Dify رسميًا', 'الدعم الفني المهني'],
|
||||
},
|
||||
},
|
||||
vectorSpace: {
|
||||
fullTip: 'مساحة المتجه ممتلئة.',
|
||||
fullSolution: 'قم بترقية خطتك للحصول على مساحة أكبر.',
|
||||
},
|
||||
apps: {
|
||||
fullTip1: 'ترقية لإنشاء المزيد من التطبيقات',
|
||||
fullTip1des: 'لقد وصلت إلى الحد الأقصى لبناء التطبيقات في هذه الخطة',
|
||||
fullTip2: 'تم الوصول إلى حد الخطة',
|
||||
fullTip2des: 'يوصى بتنظيف التطبيقات غير النشطة لتحرير الاستخدام، أو الاتصال بنا.',
|
||||
contactUs: 'اتصل بنا',
|
||||
},
|
||||
annotatedResponse: {
|
||||
fullTipLine1: 'قم بترقية خطتك لـ',
|
||||
fullTipLine2: 'التعليق على المزيد من المحادثات.',
|
||||
quotaTitle: 'حصة رد التعليقات التوضيحية',
|
||||
},
|
||||
viewBillingTitle: 'الفوترة والاشتراكات',
|
||||
viewBillingDescription: 'إدارة طرق الدفع والفواتير وتغييرات الاشتراك',
|
||||
viewBillingAction: 'يدير',
|
||||
upgrade: {
|
||||
uploadMultiplePages: {
|
||||
title: 'قم بالترقية لتحميل عدة مستندات دفعة واحدة',
|
||||
description: 'لقد وصلت إلى حد التحميل — يمكن اختيار ورفع مستند واحد فقط في كل مرة على الخطة الحالية الخاصة بك.',
|
||||
},
|
||||
uploadMultipleFiles: {
|
||||
title: 'قم بالترقية لفتح ميزة تحميل المستندات دفعة واحدة',
|
||||
description: 'قم بتحميل المزيد من المستندات دفعة واحدة لتوفير الوقت وتحسين الكفاءة.',
|
||||
},
|
||||
addChunks: {
|
||||
title: 'قم بالترقية لمواصلة إضافة المقاطع',
|
||||
description: 'لقد وصلت إلى الحد الأقصى لإضافة الأجزاء لهذا الخطة.',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,788 @@
|
|||
const translation = {
|
||||
theme: {
|
||||
theme: 'السمة',
|
||||
light: 'فاتح',
|
||||
dark: 'داكن',
|
||||
auto: 'النظام',
|
||||
},
|
||||
api: {
|
||||
success: 'نجاح',
|
||||
actionSuccess: 'نجح الإجراء',
|
||||
saved: 'تم الحفظ',
|
||||
create: 'تم الإنشاء',
|
||||
remove: 'تمت الإزالة',
|
||||
},
|
||||
operation: {
|
||||
create: 'إنشاء',
|
||||
confirm: 'تأكيد',
|
||||
cancel: 'إلغاء',
|
||||
clear: 'مسح',
|
||||
save: 'حفظ',
|
||||
yes: 'نعم',
|
||||
no: 'لا',
|
||||
deleteConfirmTitle: 'حذف؟',
|
||||
confirmAction: 'يرجى تأكيد الإجراء الخاص بك.',
|
||||
saveAndEnable: 'حفظ وتمكين',
|
||||
edit: 'تعديل',
|
||||
add: 'إضافة',
|
||||
added: 'تمت الإضافة',
|
||||
refresh: 'إعادة تشغيل',
|
||||
reset: 'إعادة تعيين',
|
||||
search: 'بحث',
|
||||
noSearchResults: 'لم يتم العثور على {{content}}',
|
||||
resetKeywords: 'إعادة تعيين الكلمات الرئيسية',
|
||||
selectCount: 'تم تحديد {{count}}',
|
||||
searchCount: 'ابحث عن {{count}} {{content}}',
|
||||
noSearchCount: '0 {{content}}',
|
||||
change: 'تغيير',
|
||||
remove: 'إزالة',
|
||||
send: 'إرسال',
|
||||
copy: 'نسخ',
|
||||
copied: 'تم النسخ',
|
||||
lineBreak: 'فاصل أسطر',
|
||||
sure: 'أنا متأكد',
|
||||
download: 'تنزيل',
|
||||
downloadSuccess: 'اكتمل التنزيل.',
|
||||
downloadFailed: 'فشل التنزيل. يرجى المحاولة مرة أخرى لاحقًا.',
|
||||
viewDetails: 'عرض التفاصيل',
|
||||
delete: 'حذف',
|
||||
now: 'الآن',
|
||||
deleteApp: 'حذف التطبيق',
|
||||
settings: 'الإعدادات',
|
||||
setup: 'إعداد',
|
||||
config: 'تكوين',
|
||||
getForFree: 'احصل عليه مجانا',
|
||||
reload: 'إعادة تحميل',
|
||||
ok: 'موافق',
|
||||
log: 'سجل',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
params: 'معلمات',
|
||||
duplicate: 'تكرار',
|
||||
rename: 'إعادة تسمية',
|
||||
audioSourceUnavailable: 'مصدر الصوت غير متاح',
|
||||
close: 'إغلاق',
|
||||
copyImage: 'نسخ الصورة',
|
||||
imageCopied: 'تم نسخ الصورة',
|
||||
zoomOut: 'تصغير',
|
||||
zoomIn: 'تكبير',
|
||||
openInNewTab: 'فتح في علامة تبويب جديدة',
|
||||
in: 'في',
|
||||
saveAndRegenerate: 'حفظ وإعادة إنشاء القطع الفرعية',
|
||||
view: 'عرض',
|
||||
viewMore: 'عرض المزيد',
|
||||
regenerate: 'إعادة إنشاء',
|
||||
submit: 'إرسال',
|
||||
skip: 'تخطي',
|
||||
format: 'تنسيق',
|
||||
more: 'المزيد',
|
||||
selectAll: 'تحديد الكل',
|
||||
deSelectAll: 'إلغاء تحديد الكل',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} مطلوب',
|
||||
urlError: 'يجب أن يبدأ العنوان بـ http:// أو https://',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'يرجى الإدخال',
|
||||
select: 'يرجى التحديد',
|
||||
search: 'بحث...',
|
||||
},
|
||||
noData: 'لا توجد بيانات',
|
||||
label: {
|
||||
optional: '(اختياري)',
|
||||
},
|
||||
voice: {
|
||||
language: {
|
||||
zhHans: 'الصينية',
|
||||
zhHant: 'الصينية التقليدية',
|
||||
enUS: 'الإنجليزية',
|
||||
deDE: 'الألمانية',
|
||||
frFR: 'الفرنسية',
|
||||
esES: 'الإسبانية',
|
||||
itIT: 'الإيطالية',
|
||||
thTH: 'التايلاندية',
|
||||
idID: 'الإندونيسية',
|
||||
jaJP: 'اليابانية',
|
||||
koKR: 'الكورية',
|
||||
ptBR: 'البرتغالية',
|
||||
ruRU: 'الروسية',
|
||||
ukUA: 'الأوكرانية',
|
||||
viVN: 'الفيتنامية',
|
||||
plPL: 'البولندية',
|
||||
roRO: 'الرومانية',
|
||||
hiIN: 'الهندية',
|
||||
trTR: 'التركية',
|
||||
faIR: 'الفارسية',
|
||||
},
|
||||
},
|
||||
unit: {
|
||||
char: 'أحرف',
|
||||
},
|
||||
actionMsg: {
|
||||
noModification: 'لا توجد تعديلات في الوقت الحالي.',
|
||||
modifiedSuccessfully: 'تم التعديل بنجاح',
|
||||
modifiedUnsuccessfully: 'فشل التعديل',
|
||||
copySuccessfully: 'تم النسخ بنجاح',
|
||||
paySucceeded: 'نجح الدفع',
|
||||
payCancelled: 'تم إلغاء الدفع',
|
||||
generatedSuccessfully: 'تم الإنشاء بنجاح',
|
||||
generatedUnsuccessfully: 'فشل الإنشاء',
|
||||
},
|
||||
model: {
|
||||
params: {
|
||||
temperature: 'درجة الحرارة',
|
||||
temperatureTip:
|
||||
'تتحكم في العشوائية: يؤدي التخفيض إلى إكمالات أقل عشوائية. مع اقتراب درجة الحرارة من الصفر، سيصبح النموذج حتميًا ومتكررًا.',
|
||||
top_p: 'أعلى P',
|
||||
top_pTip:
|
||||
'تتحكم في التنوع عبر عينات النواة: 0.5 تعني أنه يتم النظر في نصف جميع الخيارات المرجحة للاحتمالية.',
|
||||
presence_penalty: 'عقوبة الحضور',
|
||||
presence_penaltyTip:
|
||||
'مقدار معاقبة الرموز الجديدة بناءً على ما إذا كانت تظهر في النص حتى الآن.\nيزيد من احتمال تحدث النموذج عن مواضيع جديدة.',
|
||||
frequency_penalty: 'عقوبة التردد',
|
||||
frequency_penaltyTip:
|
||||
'مقدار معاقبة الرموز الجديدة بناءً على ترددها الحالي في النص حتى الآن.\nيقلل من احتمال تكرار النموذج لنفس السطر حرفيًا.',
|
||||
max_tokens: 'أقصى رمز',
|
||||
max_tokensTip:
|
||||
'يستخدم للحد من الطول الأقصى للرد، بالرموز. \nقد تحد القيم الأكبر من المساحة المتبقية للكلمات السريعة وسجلات الدردشة والمعرفة. \nيوصى بضبطه أقل من الثلثين\ngpt-4-1106-preview، gpt-4-vision-preview أقصى رمز (إدخال 128k إخراج 4k)',
|
||||
maxTokenSettingTip: 'إعداد الرموز القصوى الخاص بك مرتفع، مما قد يحد من المساحة للمطالبات والاستعلامات والبيانات. فكر في ضبطه أقل من 2/3.',
|
||||
setToCurrentModelMaxTokenTip: 'يتم تحديث الحد الأقصى للرموز إلى 80٪ من الحد الأقصى لرموز النموذج الحالي {{maxToken}}.',
|
||||
stop_sequences: 'تسلسلات التوقف',
|
||||
stop_sequencesTip: 'ما يصل إلى أربعة تسلسلات حيث ستتوقف API عن توليد المزيد من الرموز. لن يحتوي النص المرتجع على تسلسل التوقف.',
|
||||
stop_sequencesPlaceholder: 'أدخل التسلسل واضغط على Tab',
|
||||
},
|
||||
tone: {
|
||||
Creative: 'إبداعي',
|
||||
Balanced: 'متوازن',
|
||||
Precise: 'دقيق',
|
||||
Custom: 'مخصص',
|
||||
},
|
||||
addMoreModel: 'انتقل إلى الإعدادات لإضافة المزيد من النماذج',
|
||||
settingsLink: 'إعدادات مزود النموذج',
|
||||
capabilities: 'قدرات متعددة الوسائط',
|
||||
},
|
||||
menus: {
|
||||
status: 'بيتا',
|
||||
explore: 'استكشاف',
|
||||
apps: 'الاستوديو',
|
||||
appDetail: 'تفاصيل التطبيق',
|
||||
account: 'الحساب',
|
||||
plugins: 'الإضافات',
|
||||
exploreMarketplace: 'استكشاف السوق',
|
||||
pluginsTips: 'ادمج الإضافات الخارجية أو أنشئ إضافات AI متوافقة مع ChatGPT.',
|
||||
datasets: 'المعرفة',
|
||||
datasetsTips: 'قريباً: استيراد بيانات النص الخاصة بك أو كتابة البيانات في الوقت الفعلي عبر Webhook لتحسين سياق LLM.',
|
||||
newApp: 'تطبيق جديد',
|
||||
newDataset: 'إنشاء معرفة',
|
||||
tools: 'الأدوات',
|
||||
},
|
||||
userProfile: {
|
||||
settings: 'الإعدادات',
|
||||
contactUs: 'اتصل بنا',
|
||||
emailSupport: 'دعم البريد الإلكتروني',
|
||||
workspace: 'مساحة العمل',
|
||||
createWorkspace: 'إنشاء مساحة عمل',
|
||||
helpCenter: 'عرض المستندات',
|
||||
support: 'دعم',
|
||||
compliance: 'الامتثال',
|
||||
forum: 'المنتدى',
|
||||
roadmap: 'خارطة الطريق',
|
||||
github: 'GitHub',
|
||||
community: 'المجتمع',
|
||||
about: 'حول',
|
||||
logout: 'تسجيل الخروج',
|
||||
},
|
||||
compliance: {
|
||||
soc2Type1: 'تقرير SOC 2 النوع الأول',
|
||||
soc2Type2: 'تقرير SOC 2 النوع الثاني',
|
||||
iso27001: 'شهادة ISO 27001:2022',
|
||||
gdpr: 'GDPR DPA',
|
||||
sandboxUpgradeTooltip: 'متاح فقط مع خطة المحترفين أو الفريق.',
|
||||
professionalUpgradeTooltip: 'متاح فقط مع خطة الفريق أو أعلى.',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'عام',
|
||||
workplaceGroup: 'مساحة العمل',
|
||||
generalGroup: 'عام',
|
||||
account: 'حسابي',
|
||||
members: 'الأعضاء',
|
||||
billing: 'الفوترة',
|
||||
integrations: 'التكاملات',
|
||||
language: 'اللغة',
|
||||
provider: 'مزود النموذج',
|
||||
dataSource: 'مصدر البيانات',
|
||||
plugin: 'الإضافات',
|
||||
apiBasedExtension: 'ملحق API',
|
||||
},
|
||||
account: {
|
||||
account: 'الحساب',
|
||||
myAccount: 'حسابي',
|
||||
studio: 'الاستوديو',
|
||||
avatar: 'الصورة الرمزية',
|
||||
name: 'الاسم',
|
||||
email: 'البريد الإلكتروني',
|
||||
password: 'كلمة المرور',
|
||||
passwordTip: 'يمكنك تعيين كلمة مرور دائمية إذا كنت لا ترغب في استخدام رموز تسجيل الدخول المؤقتة',
|
||||
setPassword: 'تعيين كلمة مرور',
|
||||
resetPassword: 'إعادة تعيين كلمة المرور',
|
||||
currentPassword: 'كلمة المرور الحالية',
|
||||
newPassword: 'كلمة مرور جديدة',
|
||||
confirmPassword: 'تأكيد كلمة المرور',
|
||||
notEqual: 'كلمتا المرور مختلفتان.',
|
||||
langGeniusAccount: 'بيانات الحساب',
|
||||
langGeniusAccountTip: 'بيانات المستخدم الخاصة بحسابك.',
|
||||
editName: 'تعديل الاسم',
|
||||
showAppLength: 'عرض {{length}} تطبيقات',
|
||||
delete: 'حذف الحساب',
|
||||
deleteTip: 'يرجى ملاحظة أنه بمجرد التأكيد، بصفتك مالكًا لأي مساحات عمل، سيتم جدولة مساحات العمل الخاصة بك في قائمة انتظار للحذف الدائم، وسيتم جدولة جميع بيانات المستخدم الخاصة بك للحذف الدائم.',
|
||||
deletePrivacyLinkTip: 'لمزيد من المعلومات حول كيفية تعاملنا مع بياناتك، يرجى الاطلاع على ',
|
||||
deletePrivacyLink: 'سياسة الخصوصية.',
|
||||
deleteSuccessTip: 'يحتاج حسابك إلى وقت للانتهاء من الحذف. سنرسل إليك بريدًا إلكترونيًا عندما ينتهي كل شيء.',
|
||||
deleteLabel: 'للتأكيد، يرجى كتابة بريدك الإلكتروني أدناه',
|
||||
deletePlaceholder: 'يرجى إدخال بريدك الإلكتروني',
|
||||
sendVerificationButton: 'إرسال رمز التحقق',
|
||||
verificationLabel: 'رمز التحقق',
|
||||
verificationPlaceholder: 'الصق الرمز المكون من 6 أرقام',
|
||||
permanentlyDeleteButton: 'حذف الحساب نهائيًا',
|
||||
feedbackTitle: 'تعليق',
|
||||
feedbackLabel: 'أخبرنا لماذا حذفت حسابك؟',
|
||||
feedbackPlaceholder: 'اختياري',
|
||||
editWorkspaceInfo: 'تعديل معلومات مساحة العمل',
|
||||
workspaceName: 'اسم مساحة العمل',
|
||||
workspaceIcon: 'رمز مساحة العمل',
|
||||
changeEmail: {
|
||||
title: 'تغيير البريد الإلكتروني',
|
||||
verifyEmail: 'تحقق من بريدك الإلكتروني الحالي',
|
||||
newEmail: 'إعداد عنوان بريد إلكتروني جديد',
|
||||
verifyNew: 'تحقق من بريدك الإلكتروني الجديد',
|
||||
authTip: 'بمجرد تغيير بريدك الإلكتروني، لن تتمكن حسابات Google أو GitHub المرتبطة ببريدك الإلكتروني القديم من تسجيل الدخول إلى هذا الحساب.',
|
||||
content1: 'إذا تابعت، فسنرسل رمز تحقق إلى <email>{{email}}</email> لإعادة المصادقة.',
|
||||
content2: 'بريدك الإلكتروني الحالي هو <email>{{email}}</email>. تم إرسال رمز التحقق إلى عنوان البريد الإلكتروني هذا.',
|
||||
content3: 'أدخل بريدًا إلكترونيًا جديدًا وسنرسل لك رمز التحقق.',
|
||||
content4: 'لقد أرسلنا لك للتو رمز تحقق مؤقت إلى <email>{{email}}</email>.',
|
||||
codeLabel: 'رمز التحقق',
|
||||
codePlaceholder: 'الصق الرمز المكون من 6 أرقام',
|
||||
emailLabel: 'بريد إلكتروني جديد',
|
||||
emailPlaceholder: 'أدخل بريدًا إلكترونيًا جديدًا',
|
||||
existingEmail: 'مستخدم بهذا البريد الإلكتروني موجود بالفعل.',
|
||||
unAvailableEmail: 'هذا البريد الإلكتروني غير متاح مؤقتًا.',
|
||||
sendVerifyCode: 'إرسال رمز التحقق',
|
||||
continue: 'متابعة',
|
||||
changeTo: 'تغيير إلى {{email}}',
|
||||
resendTip: 'لم تتلق رمزًا؟',
|
||||
resendCount: 'إعادة إرسال في {{count}} ثانية',
|
||||
resend: 'إعادة إرسال',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
team: 'الفريق',
|
||||
invite: 'إضافة',
|
||||
name: 'الاسم',
|
||||
lastActive: 'آخر نشاط',
|
||||
role: 'الأدوار',
|
||||
pending: 'قيد الانتظار...',
|
||||
owner: 'المالك',
|
||||
admin: 'المسؤول',
|
||||
adminTip: 'يمكنه بناء التطبيقات وإدارة إعدادات الفريق',
|
||||
normal: 'عادي',
|
||||
normalTip: 'يمكنه استخدام التطبيقات فقط، ولا يمكنه بناء التطبيقات',
|
||||
builder: 'باني',
|
||||
builderTip: 'يمكنه بناء وتعديل تطبيقاته الخاصة',
|
||||
editor: 'محرر',
|
||||
editorTip: 'يمكنه بناء وتعديل التطبيقات',
|
||||
datasetOperator: 'مسؤول المعرفة',
|
||||
datasetOperatorTip: 'يمكنه إدارة قاعدة المعرفة فقط',
|
||||
inviteTeamMember: 'إضافة عضو فريق',
|
||||
inviteTeamMemberTip: 'يمكنهم الوصول إلى بيانات فريقك مباشرة بعد تسجيل الدخول.',
|
||||
emailNotSetup: 'لم يتم إعداد خادم البريد الإلكتروني، لذا لا يمكن إرسال رسائل بريد إلكتروني للدعوة. يرجى إخطار المستخدمين برابط الدعوة الذي سيتم إصداره بعد الدعوة بدلاً من ذلك.',
|
||||
email: 'البريد الإلكتروني',
|
||||
emailInvalid: 'تنسيق البريد الإلكتروني غير صالح',
|
||||
emailPlaceholder: 'يرجى إدخال رسائل البريد الإلكتروني',
|
||||
sendInvite: 'إرسال دعوة',
|
||||
invitedAsRole: 'تمت الدعوة كمستخدم {{role}}',
|
||||
invitationSent: 'تم إرسال الدعوة',
|
||||
invitationSentTip: 'تم إرسال الدعوة، ويمكنهم تسجيل الدخول إلى Dify للوصول إلى بيانات فريقك.',
|
||||
invitationLink: 'رابط الدعوة',
|
||||
failedInvitationEmails: 'لم تتم دعوة المستخدمين أدناه بنجاح',
|
||||
ok: 'موافق',
|
||||
removeFromTeam: 'إزالة من الفريق',
|
||||
removeFromTeamTip: 'سيتم إزالة وصول الفريق',
|
||||
setAdmin: 'تعيين كمسؤول',
|
||||
setMember: 'تعيين كعضو عادي',
|
||||
setBuilder: 'تعيين كباني',
|
||||
setEditor: 'تعيين كمحرر',
|
||||
disInvite: 'إلغاء الدعوة',
|
||||
deleteMember: 'حذف العضو',
|
||||
you: '(أنت)',
|
||||
transferOwnership: 'نقل الملكية',
|
||||
transferModal: {
|
||||
title: 'نقل ملكية مساحة العمل',
|
||||
warning: 'أنت على وشك نقل ملكية "{{workspace}}". يسري هذا المفعول فورًا ولا يمكن التراجع عنه.',
|
||||
warningTip: 'ستصبح عضوًا مسؤولاً، وسيتمتع المالك الجديد بالتحكم الكامل.',
|
||||
sendTip: 'إذا تابعت، فسنرسل رمز تحقق إلى <email>{{email}}</email> لإعادة المصادقة.',
|
||||
verifyEmail: 'تحقق من بريدك الإلكتروني الحالي',
|
||||
verifyContent: 'بريدك الإلكتروني الحالي هو <email>{{email}}</email>.',
|
||||
verifyContent2: 'سنرسل رمز تحقق مؤقت إلى هذا البريد الإلكتروني لإعادة المصادقة.',
|
||||
codeLabel: 'رمز التحقق',
|
||||
codePlaceholder: 'الصق الرمز المكون من 6 أرقام',
|
||||
resendTip: 'لم تتلق رمزًا؟',
|
||||
resendCount: 'إعادة إرسال في {{count}} ثانية',
|
||||
resend: 'إعادة إرسال',
|
||||
transferLabel: 'نقل ملكية مساحة العمل إلى',
|
||||
transferPlaceholder: 'حدد عضو مساحة عمل...',
|
||||
sendVerifyCode: 'إرسال رمز التحقق',
|
||||
continue: 'متابعة',
|
||||
transfer: 'نقل ملكية مساحة العمل',
|
||||
},
|
||||
},
|
||||
feedback: {
|
||||
title: 'تقديم تعليق',
|
||||
subtitle: 'من فضلك أخبرنا ما الخطأ في هذه الاستجابة',
|
||||
content: 'محتوى التعليق',
|
||||
placeholder: 'يرجى وصف ما حدث خطأ أو كيف يمكننا التحسين...',
|
||||
},
|
||||
integrations: {
|
||||
connected: 'متصل',
|
||||
google: 'Google',
|
||||
googleAccount: 'تسجيل الدخول بحساب Google',
|
||||
github: 'GitHub',
|
||||
githubAccount: 'تسجيل الدخول بحساب GitHub',
|
||||
connect: 'اتصال',
|
||||
},
|
||||
language: {
|
||||
displayLanguage: 'لغة العرض',
|
||||
timezone: 'المنطقة الزمنية',
|
||||
},
|
||||
provider: {
|
||||
apiKey: 'مفتاح API',
|
||||
enterYourKey: 'أدخل مفتاح API الخاص بك هنا',
|
||||
invalidKey: 'مفتاح OpenAI API غير صالح',
|
||||
validatedError: 'فشل التحقق: ',
|
||||
validating: 'جارٍ التحقق من المفتاح...',
|
||||
saveFailed: 'فشل حفظ مفتاح api',
|
||||
apiKeyExceedBill: 'لا يحتوي مفتاح API هذا على حصة متاحة، يرجى القراءة',
|
||||
addKey: 'إضافة مفتاح',
|
||||
comingSoon: 'قريباً',
|
||||
editKey: 'تعديل',
|
||||
invalidApiKey: 'مفتاح API غير صالح',
|
||||
azure: {
|
||||
apiBase: 'قاعدة API',
|
||||
apiBasePlaceholder: 'عنوان URL لقاعدة API لنقطة نهاية Azure OpenAI الخاصة بك.',
|
||||
apiKey: 'مفتاح API',
|
||||
apiKeyPlaceholder: 'أدخل مفتاح API الخاص بك هنا',
|
||||
helpTip: 'تعلم خدمة Azure OpenAI',
|
||||
},
|
||||
openaiHosted: {
|
||||
openaiHosted: 'OpenAI المستضافة',
|
||||
onTrial: 'في التجربة',
|
||||
exhausted: 'نفدت الحصة',
|
||||
desc: 'تسمح لك خدمة استضافة OpenAI المقدمة من Dify باستخدام نماذج مثل GPT-3.5. قبل نفاد حصة التجربة الخاصة بك، تحتاج إلى إعداد موفري نماذج آخرين.',
|
||||
callTimes: 'أوقات الاتصال',
|
||||
usedUp: 'نفدت حصة التجربة. أضف مزود النموذج الخاص بك.',
|
||||
useYourModel: 'تستخدم حاليًا مزود النموذج الخاص بك.',
|
||||
close: 'إغلاق',
|
||||
},
|
||||
anthropicHosted: {
|
||||
anthropicHosted: 'Anthropic Claude',
|
||||
onTrial: 'في التجربة',
|
||||
exhausted: 'نفدت الحصة',
|
||||
desc: 'نموذج قوي يتفوق في مجموعة واسعة من المهام من الحوار المعقد وإنشاء المحتوى الإبداعي إلى التعليمات التفصيلية.',
|
||||
callTimes: 'أوقات الاتصال',
|
||||
usedUp: 'نفدت حصة التجربة. أضف مزود النموذج الخاص بك.',
|
||||
useYourModel: 'تستخدم حاليًا مزود النموذج الخاص بك.',
|
||||
close: 'إغلاق',
|
||||
trialQuotaTip: 'ستنتهي حصة التجربة الخاصة بك في Anthropic في 2025/03/17 ولن تكون متاحة بعد ذلك. يرجى الاستفادة منها في الوقت المحدد.',
|
||||
},
|
||||
anthropic: {
|
||||
using: 'قدرة التضمين تستخدم',
|
||||
enableTip: 'لتمكين نموذج Anthropic، تحتاج إلى الارتباط بـ OpenAI أو خدمة Azure OpenAI أولاً.',
|
||||
notEnabled: 'غير ممكن',
|
||||
keyFrom: 'احصل على مفتاح API الخاص بك من Anthropic',
|
||||
},
|
||||
encrypted: {
|
||||
front: 'سيتم تشفير مفتاح API الخاص بك وتخزينه باستخدام تقنية',
|
||||
back: '.',
|
||||
},
|
||||
},
|
||||
modelProvider: {
|
||||
notConfigured: 'لم يتم تكوين نموذج النظام بالكامل بعد',
|
||||
systemModelSettings: 'إعدادات نموذج النظام',
|
||||
systemModelSettingsLink: 'لماذا من الضروري إعداد نموذج النظام؟',
|
||||
selectModel: 'اختر نموذجك',
|
||||
setupModelFirst: 'يرجى إعداد نموذجك أولاً',
|
||||
systemReasoningModel: {
|
||||
key: 'نموذج التفكير النظامي',
|
||||
tip: 'تعيين نموذج الاستنتاج الافتراضي لاستخدامه لإنشاء التطبيقات، بالإضافة إلى ميزات مثل إنشاء اسم الحوار واقتراح السؤال التالي ستستخدم أيضًا نموذج الاستنتاج الافتراضي.',
|
||||
},
|
||||
embeddingModel: {
|
||||
key: 'نموذج التضمين',
|
||||
tip: 'تعيين النموذج الافتراضي لمعالجة تضمين المستندات للمعرفة، حيث يستخدم كل من استرجاع واستيراد المعرفة نموذج التضمين هذا لمعالجة التوجيه. سيؤدي التبديل إلى أن يكون البعد المتجه بين المعرفة المستوردة والسؤال غير متسق، مما يؤدي إلى فشل الاسترجاع. لتجنب فشل الاسترجاع، يرجى عدم تبديل هذا النموذج حسب الرغبة.',
|
||||
required: 'نموذج التضمين مطلوب',
|
||||
},
|
||||
speechToTextModel: {
|
||||
key: 'نموذج تحويل الكلام إلى نص',
|
||||
tip: 'تعيين النموذج الافتراضي لإدخال تحويل الكلام إلى نص في المحادثة.',
|
||||
},
|
||||
ttsModel: {
|
||||
key: 'نموذج تحويل النص إلى كلام',
|
||||
tip: 'تعيين النموذج الافتراضي لإدخال تحويل النص إلى كلام في المحادثة.',
|
||||
},
|
||||
rerankModel: {
|
||||
key: 'نموذج إعادة الترتيب',
|
||||
tip: 'سيعيد نموذج إعادة الترتيب ترتيب قائمة المستندات المرشحة بناءً على المطابقة الدلالية مع استعلام المستخدم، مما يحسن نتائج الترتيب الدلالي',
|
||||
},
|
||||
apiKey: 'مفتاح API',
|
||||
quota: 'حصة',
|
||||
searchModel: 'نموذج البحث',
|
||||
noModelFound: 'لم يتم العثور على نموذج لـ {{model}}',
|
||||
models: 'النماذج',
|
||||
showMoreModelProvider: 'عرض المزيد من مزودي النماذج',
|
||||
selector: {
|
||||
tip: 'تمت إزالة هذا النموذج. يرجى إضافة نموذج أو تحديد نموذج آخر.',
|
||||
emptyTip: 'لا توجد نماذج متاحة',
|
||||
emptySetting: 'يرجى الانتقال إلى الإعدادات للتكوين',
|
||||
rerankTip: 'يرجى إعداد نموذج إعادة الترتيب',
|
||||
},
|
||||
card: {
|
||||
quota: 'حصة',
|
||||
onTrial: 'في التجربة',
|
||||
paid: 'مدفوع',
|
||||
quotaExhausted: 'نفدت الحصة',
|
||||
callTimes: 'أوقات الاتصال',
|
||||
tokens: 'رموز',
|
||||
buyQuota: 'شراء حصة',
|
||||
priorityUse: 'أولوية الاستخدام',
|
||||
removeKey: 'إزالة مفتاح API',
|
||||
tip: 'ستعطى الأولوية للحصة المدفوعة. سيتم استخدام الحصة التجريبية بعد نفاد الحصة المدفوعة.',
|
||||
},
|
||||
item: {
|
||||
deleteDesc: 'يتم استخدام {{modelName}} كنماذج تفكير النظام. لن تكون بعض الوظائف متاحة بعد الإزالة. يرجى التأكيد.',
|
||||
freeQuota: 'حصة مجانية',
|
||||
},
|
||||
addApiKey: 'أضف مفتاح API الخاص بك',
|
||||
invalidApiKey: 'مفتاح API غير صالح',
|
||||
encrypted: {
|
||||
front: 'سيتم تشفير مفتاح API الخاص بك وتخزينه باستخدام تقنية',
|
||||
back: '.',
|
||||
},
|
||||
freeQuota: {
|
||||
howToEarn: 'كيف تكسب',
|
||||
},
|
||||
addMoreModelProvider: 'أضف المزيد من مزودي النماذج',
|
||||
addModel: 'إضافة نموذج',
|
||||
modelsNum: '{{num}} نماذج',
|
||||
showModels: 'عرض النماذج',
|
||||
showModelsNum: 'عرض {{num}} نماذج',
|
||||
collapse: 'طي',
|
||||
config: 'تكوين',
|
||||
modelAndParameters: 'النموذج والمعلمات',
|
||||
model: 'النموذج',
|
||||
featureSupported: '{{feature}} مدعوم',
|
||||
callTimes: 'أوقات الاتصال',
|
||||
credits: 'أرصدة الرسائل',
|
||||
buyQuota: 'شراء حصة',
|
||||
getFreeTokens: 'احصل على رموز مجانية',
|
||||
priorityUsing: 'أولوية الاستخدام',
|
||||
deprecated: 'مهمل',
|
||||
confirmDelete: 'تأكيد الحذف؟',
|
||||
quotaTip: 'الرموز المجانية المتاحة المتبقية',
|
||||
loadPresets: 'تحميل الإعدادات المسبقة',
|
||||
parameters: 'المعلمات',
|
||||
loadBalancing: 'موازنة التحميل',
|
||||
loadBalancingDescription: 'تكوين بيانات اعتماد متعددة للنموذج واستدعاؤها تلقائيًا. ',
|
||||
loadBalancingHeadline: 'موازنة التحميل',
|
||||
configLoadBalancing: 'تكوين موازنة التحميل',
|
||||
modelHasBeenDeprecated: 'تم إهمال هذا النموذج',
|
||||
providerManaged: 'مدار من قبل المزود',
|
||||
providerManagedDescription: 'استخدم مجموعة واحدة من بيانات الاعتماد المقدمة من مزود النموذج.',
|
||||
defaultConfig: 'التكوين الافتراضي',
|
||||
apiKeyStatusNormal: 'حالة مفتاح API طبيعية',
|
||||
apiKeyRateLimit: 'تم الوصول إلى حد المعدل، متاح بعد {{seconds}} ثانية',
|
||||
addConfig: 'إضافة تكوين',
|
||||
editConfig: 'تعديل التكوين',
|
||||
loadBalancingLeastKeyWarning: 'لتمكين موازنة التحميل، يجب تمكين مفتاحين على الأقل.',
|
||||
loadBalancingInfo: 'بشكل افتراضي، تستخدم موازنة التحميل استراتيجية Round-robin. إذا تم تشغيل تحديد المعدل، فسيتم تطبيق فترة تباطؤ مدتها دقيقة واحدة.',
|
||||
upgradeForLoadBalancing: 'قم بترقية خطتك لتمكين موازنة التحميل.',
|
||||
toBeConfigured: 'ليتم تكوينه',
|
||||
configureTip: 'قم بإعداد مفتاح api أو أضف نموذجًا للاستخدام',
|
||||
installProvider: 'تثبيت مزودي النماذج',
|
||||
installDataSourceProvider: 'تثبيت مزودي مصادر البيانات',
|
||||
discoverMore: 'اكتشف المزيد في ',
|
||||
emptyProviderTitle: 'لم يتم إعداد مزود النموذج',
|
||||
emptyProviderTip: 'يرجى تثبيت مزود نموذج أولاً.',
|
||||
auth: {
|
||||
unAuthorized: 'غير مصرح به',
|
||||
authRemoved: 'تمت إزالة المصادقة',
|
||||
apiKeys: 'مفاتيح API',
|
||||
addApiKey: 'إضافة مفتاح API',
|
||||
addModel: 'إضافة نموذج',
|
||||
addNewModel: 'إضافة نموذج جديد',
|
||||
addCredential: 'إضافة بيانات اعتماد',
|
||||
addModelCredential: 'إضافة بيانات اعتماد النموذج',
|
||||
editModelCredential: 'تعديل بيانات اعتماد النموذج',
|
||||
modelCredentials: 'بيانات اعتماد النموذج',
|
||||
modelCredential: 'بيانات اعتماد النموذج',
|
||||
configModel: 'تكوين النموذج',
|
||||
configLoadBalancing: 'تكوين موازنة التحميل',
|
||||
authorizationError: 'خطأ في التفويض',
|
||||
specifyModelCredential: 'تحديد بيانات اعتماد النموذج',
|
||||
specifyModelCredentialTip: 'استخدم بيانات اعتماد نموذج مكونة.',
|
||||
providerManaged: 'مدار من قبل المزود',
|
||||
providerManagedTip: 'يتم استضافة التكوين الحالي بواسطة المزود.',
|
||||
apiKeyModal: {
|
||||
title: 'تكوين تفويض مفتاح API',
|
||||
desc: 'بعد تكوين بيانات الاعتماد، يمكن لجميع الأعضاء داخل مساحة العمل استخدام هذا النموذج عند تنظيم التطبيقات.',
|
||||
addModel: 'إضافة نموذج',
|
||||
},
|
||||
manageCredentials: 'إدارة بيانات الاعتماد',
|
||||
customModelCredentials: 'بيانات اعتماد النموذج المخصصة',
|
||||
addNewModelCredential: 'إضافة بيانات اعتماد نموذج جديدة',
|
||||
removeModel: 'إزالة النموذج',
|
||||
selectModelCredential: 'تحديد بيانات اعتماد النموذج',
|
||||
customModelCredentialsDeleteTip: 'بيانات الاعتماد قيد الاستخدام ولا يمكن حذفها',
|
||||
},
|
||||
parametersInvalidRemoved: 'بعض المعلمات غير صالحة وتمت إزالتها',
|
||||
},
|
||||
dataSource: {
|
||||
add: 'إضافة مصدر بيانات',
|
||||
connect: 'اتصال',
|
||||
configure: 'تكوين',
|
||||
notion: {
|
||||
title: 'Notion',
|
||||
description: 'استخدام Notion كمصدر بيانات للمعرفة.',
|
||||
connectedWorkspace: 'مساحة العمل المتصلة',
|
||||
addWorkspace: 'إضافة مساحة عمل',
|
||||
connected: 'متصل',
|
||||
disconnected: 'غير متصل',
|
||||
changeAuthorizedPages: 'تغيير الصفحات المصرح بها',
|
||||
integratedAlert: 'تم دمج Notion عبر بيانات الاعتماد الداخلية، ولا حاجة لإعادة التفويض.',
|
||||
pagesAuthorized: 'الصفحات المصرح بها',
|
||||
sync: 'مزامنة',
|
||||
remove: 'إزالة',
|
||||
selector: {
|
||||
pageSelected: 'الصفحات المحددة',
|
||||
searchPages: 'بحث في الصفحات...',
|
||||
noSearchResult: 'لا توجد نتائج بحث',
|
||||
addPages: 'إضافة صفحات',
|
||||
preview: 'معاينة',
|
||||
},
|
||||
},
|
||||
website: {
|
||||
title: 'موقع الكتروني',
|
||||
description: 'استيراد المحتوى من المواقع الإلكترونية باستخدام زحف الويب.',
|
||||
with: 'مع',
|
||||
configuredCrawlers: 'الزواحف المكونة',
|
||||
active: 'نشط',
|
||||
inactive: 'غير نشط',
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
serpapi: {
|
||||
apiKey: 'مفتاح API',
|
||||
apiKeyPlaceholder: 'أدخل مفتاح API الخاص بك',
|
||||
keyFrom: 'احصل على مفتاح SerpAPI الخاص بك من صفحة حساب SerpAPI',
|
||||
},
|
||||
},
|
||||
apiBasedExtension: {
|
||||
title: 'توفر ملحقات API إدارة مركزية لواجهة برمجة التطبيقات، مما يبسط التكوين لسهولة الاستخدام عبر تطبيقات Dify.',
|
||||
link: 'تعرف على كيفية تطوير ملحق API الخاص بك.',
|
||||
add: 'إضافة ملحق API',
|
||||
selector: {
|
||||
title: 'ملحق API',
|
||||
placeholder: 'يرجى تحديد ملحق API',
|
||||
manage: 'إدارة ملحق API',
|
||||
},
|
||||
modal: {
|
||||
title: 'إضافة ملحق API',
|
||||
editTitle: 'تعديل ملحق API',
|
||||
name: {
|
||||
title: 'الاسم',
|
||||
placeholder: 'يرجى إدخال الاسم',
|
||||
},
|
||||
apiEndpoint: {
|
||||
title: 'نقطة نهاية API',
|
||||
placeholder: 'يرجى إدخال نقطة نهاية API',
|
||||
},
|
||||
apiKey: {
|
||||
title: 'مفتاح API',
|
||||
placeholder: 'يرجى إدخال مفتاح API',
|
||||
lengthError: 'لا يمكن أن يكون طول مفتاح API أقل من 5 أحرف',
|
||||
},
|
||||
},
|
||||
type: 'النوع',
|
||||
},
|
||||
about: {
|
||||
changeLog: 'سجل التغييرات',
|
||||
updateNow: 'تحديث الآن',
|
||||
nowAvailable: 'Dify {{version}} متاح الآن.',
|
||||
latestAvailable: 'Dify {{version}} هو أحدث إصدار متاح.',
|
||||
},
|
||||
appMenus: {
|
||||
overview: 'المراقبة',
|
||||
promptEng: 'تنسيق',
|
||||
apiAccess: 'وصول API',
|
||||
logAndAnn: 'السجلات والتعليقات التوضيحية',
|
||||
logs: 'السجلات',
|
||||
},
|
||||
environment: {
|
||||
testing: 'اختبار',
|
||||
development: 'تطوير',
|
||||
},
|
||||
appModes: {
|
||||
completionApp: 'مولد النص',
|
||||
chatApp: 'تطبيق الدردشة',
|
||||
},
|
||||
datasetMenus: {
|
||||
documents: 'المستندات',
|
||||
hitTesting: 'اختبار الاسترجاع',
|
||||
settings: 'الإعدادات',
|
||||
emptyTip: 'لم يتم دمج هذه المعرفة في أي تطبيق. يرجى الرجوع إلى المستند للحصول على إرشادات.',
|
||||
viewDoc: 'عرض المستندات',
|
||||
relatedApp: 'التطبيقات المرتبطة',
|
||||
noRelatedApp: 'لا توجد تطبيقات مرتبطة',
|
||||
pipeline: 'خط الأنابيب',
|
||||
},
|
||||
voiceInput: {
|
||||
speaking: 'تحدث الآن...',
|
||||
converting: 'التحويل إلى نص...',
|
||||
notAllow: 'الميكروفون غير مصرح به',
|
||||
},
|
||||
modelName: {
|
||||
'gpt-3.5-turbo': 'GPT-3.5-Turbo',
|
||||
'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K',
|
||||
'gpt-4': 'GPT-4',
|
||||
'gpt-4-32k': 'GPT-4-32K',
|
||||
'text-davinci-003': 'Text-Davinci-003',
|
||||
'text-embedding-ada-002': 'Text-Embedding-Ada-002',
|
||||
'whisper-1': 'Whisper-1',
|
||||
'claude-instant-1': 'Claude-Instant',
|
||||
'claude-2': 'Claude-2',
|
||||
},
|
||||
chat: {
|
||||
renameConversation: 'إعادة تسمية المحادثة',
|
||||
conversationName: 'اسم المحادثة',
|
||||
conversationNamePlaceholder: 'يرجى إدخال اسم المحادثة',
|
||||
conversationNameCanNotEmpty: 'اسم المحادثة مطلوب',
|
||||
citation: {
|
||||
title: 'الاستشهادات',
|
||||
linkToDataset: 'رابط المعرفة',
|
||||
characters: 'الشخصيات:',
|
||||
hitCount: 'عدد الاسترجاع:',
|
||||
vectorHash: 'تجزئة المتجه:',
|
||||
hitScore: 'درجة الاسترجاع:',
|
||||
},
|
||||
inputPlaceholder: 'تحدث إلى {{botName}}',
|
||||
thinking: 'يفكر...',
|
||||
thought: 'فكر',
|
||||
resend: 'إعادة إرسال',
|
||||
},
|
||||
promptEditor: {
|
||||
placeholder: 'اكتب كلمة المطالبة هنا، أدخل \'{\' لإدراج متغير، أدخل \'/\' لإدراج كتلة محتوى مطالبة',
|
||||
context: {
|
||||
item: {
|
||||
title: 'السياق',
|
||||
desc: 'إدراج قالب السياق',
|
||||
},
|
||||
modal: {
|
||||
title: '{{num}} معرفة في السياق',
|
||||
add: 'إضافة سياق ',
|
||||
footer: 'يمكنك إدارة السياقات في قسم السياق أدناه.',
|
||||
},
|
||||
},
|
||||
history: {
|
||||
item: {
|
||||
title: 'سجل المحادثة',
|
||||
desc: 'إدراج قالب الرسالة التاريخية',
|
||||
},
|
||||
modal: {
|
||||
title: 'مثال',
|
||||
user: 'مرحبًا',
|
||||
assistant: 'مرحبًا! كيف يمكنني مساعدتك اليوم؟',
|
||||
edit: 'تعديل أسماء أدوار المحادثة',
|
||||
},
|
||||
},
|
||||
variable: {
|
||||
item: {
|
||||
title: 'المتغيرات والأدوات الخارجية',
|
||||
desc: 'إدراج المتغيرات والأدوات الخارجية',
|
||||
},
|
||||
outputToolDisabledItem: {
|
||||
title: 'المتغيرات',
|
||||
desc: 'إدراج المتغيرات',
|
||||
},
|
||||
modal: {
|
||||
add: 'متغير جديد',
|
||||
addTool: 'أداة جديدة',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
item: {
|
||||
title: 'استعلام',
|
||||
desc: 'إدراج قالب استعلام المستخدم',
|
||||
},
|
||||
},
|
||||
existed: 'موجود بالفعل في المطالبة',
|
||||
},
|
||||
imageUploader: {
|
||||
uploadFromComputer: 'تحميل من الكمبيوتر',
|
||||
uploadFromComputerReadError: 'فشل قراءة الصورة، يرجى المحاولة مرة أخرى.',
|
||||
uploadFromComputerUploadError: 'فشل تحميل الصورة، يرجى التحميل مرة أخرى.',
|
||||
uploadFromComputerLimit: 'لا يمكن أن تتجاوز صور التحميل {{size}} ميجابايت',
|
||||
pasteImageLink: 'لصق رابط الصورة',
|
||||
pasteImageLinkInputPlaceholder: 'لصق رابط الصورة هنا',
|
||||
pasteImageLinkInvalid: 'رابط الصورة غير صالح',
|
||||
imageUpload: 'تحميل الصورة',
|
||||
},
|
||||
fileUploader: {
|
||||
uploadFromComputer: 'تحميل محلي',
|
||||
pasteFileLink: 'لصق رابط الملف',
|
||||
pasteFileLinkInputPlaceholder: 'أدخل URL...',
|
||||
uploadFromComputerReadError: 'فشل قراءة الملف، يرجى المحاولة مرة أخرى.',
|
||||
uploadFromComputerUploadError: 'فشل تحميل الملف، يرجى التحميل مرة أخرى.',
|
||||
uploadFromComputerLimit: 'تحميل {{type}} لا يمكن أن يتجاوز {{size}}',
|
||||
pasteFileLinkInvalid: 'رابط الملف غير صالح',
|
||||
fileExtensionNotSupport: 'امتداد الملف غير مدعوم',
|
||||
fileExtensionBlocked: 'تم حظر نوع الملف هذا لأسباب أمنية',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'جميع العلامات',
|
||||
addNew: 'إضافة علامة جديدة',
|
||||
noTag: 'لا توجد علامات',
|
||||
noTagYet: 'لا توجد علامات بعد',
|
||||
addTag: 'إضافة علامات',
|
||||
editTag: 'تعديل العلامات',
|
||||
manageTags: 'إدارة العلامات',
|
||||
selectorPlaceholder: 'اكتب للبحث أو الإنشاء',
|
||||
create: 'إنشاء',
|
||||
delete: 'حذف العلامة',
|
||||
deleteTip: 'العلامة قيد الاستخدام، هل تريد حذفها؟',
|
||||
created: 'تم إنشاء العلامة بنجاح',
|
||||
failed: 'فشل إنشاء العلامة',
|
||||
},
|
||||
license: {
|
||||
expiring: 'تنتهي في يوم واحد',
|
||||
expiring_plural: 'تنتهي في {{count}} أيام',
|
||||
unlimited: 'غير محدود',
|
||||
},
|
||||
pagination: {
|
||||
perPage: 'عناصر لكل صفحة',
|
||||
},
|
||||
avatar: {
|
||||
deleteTitle: 'إزالة الصورة الرمزية',
|
||||
deleteDescription: 'هل أنت متأكد أنك تريد إزالة صورة ملفك الشخصي؟ سيستخدم حسابك الصورة الرمزية الأولية الافتراضية.',
|
||||
},
|
||||
imageInput: {
|
||||
dropImageHere: 'أسقط صورتك هنا، أو',
|
||||
browse: 'تصفح',
|
||||
supportedFormats: 'يدعم PNG و JPG و JPEG و WEBP و GIF',
|
||||
},
|
||||
you: 'أنت',
|
||||
dynamicSelect: {
|
||||
error: 'فشل تحميل الخيارات',
|
||||
noData: 'لا توجد خيارات متاحة',
|
||||
loading: 'تحميل الخيارات...',
|
||||
selected: '{{count}} محدد',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
const translation = {
|
||||
custom: 'تخصيص',
|
||||
upgradeTip: {
|
||||
title: 'تحديث خطتك',
|
||||
des: 'قم بترقية خطتك لتخصيص علامتك التجارية',
|
||||
prefix: 'قم بترقية خطتك لـ',
|
||||
suffix: 'تخصيص علامتك التجارية.',
|
||||
},
|
||||
webapp: {
|
||||
title: 'تخصيص العلامة التجارية لتطبيق الويب',
|
||||
removeBrand: 'إزالة Powered by Dify',
|
||||
changeLogo: 'تغيير صورة Powered by Brand',
|
||||
changeLogoTip: 'تنسيق SVG أو PNG بحجم أدنى 40x40px',
|
||||
},
|
||||
app: {
|
||||
title: 'تخصيص العلامة التجارية لرأس التطبيق',
|
||||
changeLogoTip: 'تنسيق SVG أو PNG بحجم أدنى 80x80px',
|
||||
},
|
||||
upload: 'تحميل',
|
||||
uploading: 'جاري التحميل',
|
||||
uploadedFail: 'فشل تحميل الصورة، يرجى إعادة التحميل.',
|
||||
change: 'تغيير',
|
||||
apply: 'تطبيق',
|
||||
restore: 'استعادة الافتراضيات',
|
||||
customize: {
|
||||
contactUs: ' اتصل بنا ',
|
||||
prefix: 'لتخصيص شعار العلامة التجارية داخل التطبيق، يرجى',
|
||||
suffix: 'للترقية إلى إصدار Enterprise.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
const translation = {
|
||||
steps: {
|
||||
header: {
|
||||
fallbackRoute: 'المعرفة',
|
||||
},
|
||||
one: 'مصدر البيانات',
|
||||
two: 'معالجة المستندات',
|
||||
three: 'التنفيذ والانتهاء',
|
||||
},
|
||||
error: {
|
||||
unavailable: 'هذه المعرفة غير متاحة',
|
||||
},
|
||||
firecrawl: {
|
||||
configFirecrawl: 'تكوين 🔥Firecrawl',
|
||||
apiKeyPlaceholder: 'مفتاح API من firecrawl.dev',
|
||||
getApiKeyLinkText: 'احصل على مفتاح API الخاص بك من firecrawl.dev',
|
||||
},
|
||||
watercrawl: {
|
||||
configWatercrawl: 'تكوين Watercrawl',
|
||||
apiKeyPlaceholder: 'مفتاح API من watercrawl.dev',
|
||||
getApiKeyLinkText: 'احصل على مفتاح API الخاص بك من watercrawl.dev',
|
||||
},
|
||||
jinaReader: {
|
||||
configJinaReader: 'تكوين Jina Reader',
|
||||
apiKeyPlaceholder: 'مفتاح API من jina.ai',
|
||||
getApiKeyLinkText: 'احصل على مفتاح API المجاني الخاص بك في jina.ai',
|
||||
},
|
||||
stepOne: {
|
||||
filePreview: 'معاينة الملف',
|
||||
pagePreview: 'معاينة الصفحة',
|
||||
dataSourceType: {
|
||||
file: 'استيراد من ملف',
|
||||
notion: 'مزامنة من Notion',
|
||||
web: 'مزامنة من موقع ويب',
|
||||
},
|
||||
uploader: {
|
||||
title: 'تحميل ملف',
|
||||
button: 'اسحب وأفلت الملف أو المجلد، أو',
|
||||
buttonSingleFile: 'اسحب وأفلت الملف، أو',
|
||||
browse: 'تصفح',
|
||||
tip: 'يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها. الحد الأقصى الإجمالي {{totalCount}} ملفات.',
|
||||
validation: {
|
||||
typeError: 'نوع الملف غير مدعوم',
|
||||
size: 'الملف كبير جدًا. الحد الأقصى هو {{size}} ميجابايت',
|
||||
count: 'ملفات متعددة غير مدعومة',
|
||||
filesNumber: 'لقد وصلت إلى حد تحميل الدفعة البالغ {{filesNumber}}.',
|
||||
},
|
||||
cancel: 'إلغاء',
|
||||
change: 'تغيير',
|
||||
failed: 'فشل التحميل',
|
||||
},
|
||||
notionSyncTitle: 'Notion غير متصل',
|
||||
notionSyncTip: 'للمزامنة مع Notion، يجب إنشاء اتصال بـ Notion أولاً.',
|
||||
connect: 'الذهاب للاتصال',
|
||||
cancel: 'إلغاء',
|
||||
button: 'التالي',
|
||||
emptyDatasetCreation: 'أريد إنشاء معرفة فارغة',
|
||||
modal: {
|
||||
title: 'إنشاء معرفة فارغة',
|
||||
tip: 'لن تحتوي المعرفة الفارغة على أي مستندات، ويمكنك تحميل المستندات في أي وقت.',
|
||||
input: 'اسم المعرفة',
|
||||
placeholder: 'يرجى الإدخال',
|
||||
nameNotEmpty: 'لا يمكن أن يكون الاسم فارغًا',
|
||||
nameLengthInvalid: 'يجب أن يكون الاسم بين 1 إلى 40 حرفًا',
|
||||
cancelButton: 'إلغاء',
|
||||
confirmButton: 'إنشاء',
|
||||
failed: 'فشل الإنشاء',
|
||||
},
|
||||
website: {
|
||||
chooseProvider: 'اختر مزودًا',
|
||||
fireCrawlNotConfigured: 'Firecrawl غير مكون',
|
||||
fireCrawlNotConfiguredDescription: 'قم بتكوين Firecrawl باستخدام مفتاح API لاستخدامه.',
|
||||
jinaReaderNotConfigured: 'Jina Reader غير مكون',
|
||||
jinaReaderNotConfiguredDescription: 'قم بإعداد Jina Reader عن طريق إدخال مفتاح API المجاني للوصول.',
|
||||
waterCrawlNotConfigured: 'Watercrawl غير مكون',
|
||||
waterCrawlNotConfiguredDescription: 'قم بتكوين Watercrawl باستخدام مفتاح API لاستخدامه.',
|
||||
configure: 'تكوين',
|
||||
configureFirecrawl: 'تكوين Firecrawl',
|
||||
configureWatercrawl: 'تكوين Watercrawl',
|
||||
configureJinaReader: 'تكوين Jina Reader',
|
||||
run: 'تشغيل',
|
||||
running: 'جارٍ التشغيل',
|
||||
firecrawlTitle: 'استخراج محتوى الويب باستخدام 🔥Firecrawl',
|
||||
firecrawlDoc: 'مستندات Firecrawl',
|
||||
watercrawlTitle: 'استخراج محتوى الويب باستخدام Watercrawl',
|
||||
watercrawlDoc: 'مستندات Watercrawl',
|
||||
jinaReaderTitle: 'تحويل الموقع بالكامل إلى Markdown',
|
||||
jinaReaderDoc: 'تعرف على المزيد حول Jina Reader',
|
||||
jinaReaderDocLink: 'https://jina.ai/reader',
|
||||
useSitemap: 'استخدام خريطة الموقع',
|
||||
useSitemapTooltip: 'اتبع خريطة الموقع للزحف إلى الموقع. إذا لم يكن كذلك، سيقوم Jina Reader بالزحف بشكل متكرر بناءً على صلة الصفحة، مما يؤدي إلى صفحات أقل ولكن بجودة أعلى.',
|
||||
options: 'خيارات',
|
||||
crawlSubPage: 'الزحف إلى الصفحات الفرعية',
|
||||
limit: 'الحد',
|
||||
maxDepth: 'أقصى عمق',
|
||||
excludePaths: 'استبعاد المسارات',
|
||||
includeOnlyPaths: 'تضمين المسارات فقط',
|
||||
extractOnlyMainContent: 'استخراج المحتوى الرئيسي فقط (بدون رؤوس، قوائم تنقل، تذييلات، إلخ.)',
|
||||
exceptionErrorTitle: 'حدث استثناء أثناء تشغيل مهمة الزحف:',
|
||||
unknownError: 'خطأ غير معروف',
|
||||
totalPageScraped: 'إجمالي الصفحات التي تم كشطها:',
|
||||
selectAll: 'تحديد الكل',
|
||||
resetAll: 'إعادة تعيين الكل',
|
||||
scrapTimeInfo: 'تم كشط {{total}} صفحة في المجموع خلال {{time}} ثانية',
|
||||
preview: 'معاينة',
|
||||
maxDepthTooltip: 'أقصى عمق للزحف بالنسبة لعنوان URL المدخل. العمق 0 يكشط فقط صفحة عنوان URL المدخل، العمق 1 يكشط عنوان URL وكل شيء بعد عنوان URL المدخل + / واحد، وهكذا.',
|
||||
},
|
||||
},
|
||||
stepTwo: {
|
||||
segmentation: 'إعدادات القطعة',
|
||||
auto: 'تلقائي',
|
||||
autoDescription: 'تحديد القواعد والتقطيع والمعالجة المسبقة تلقائيًا. يوصى به للمستخدمين غير المألوفين.',
|
||||
custom: 'مخصص',
|
||||
customDescription: 'تخصيص قواعد القطع وطول القطع وقواعد المعالجة المسبقة، إلخ.',
|
||||
general: 'عام',
|
||||
generalTip: 'وضع تقطيع النص العام، القطع المسترجعة والمستردة هي نفسها.',
|
||||
parentChild: 'الأصل والطفل',
|
||||
parentChildTip: 'عند استخدام وضع الأصل والطفل، يتم استخدام القطعة الفرعية للاسترجاع ويتم استخدام القطعة الأصلية للاستدعاء كسياق.',
|
||||
parentChunkForContext: 'القطعة الأصلية للسياق',
|
||||
childChunkForRetrieval: 'القطعة الفرعية للاسترجاع',
|
||||
paragraph: 'فقرة',
|
||||
paragraphTip: 'يقسم هذا الوضع النص إلى فقرات بناءً على المحددات وأقصى طول للقطعة، باستخدام النص المقسم كقطعة أصلية للاسترجاع.',
|
||||
fullDoc: 'مستند كامل',
|
||||
fullDocTip: 'يتم استخدام المستند بأكمله كقطعة أصلية ويتم استرجاعه مباشرة. يرجى ملاحظة أنه لأسباب تتعلق بالأداء، سيتم اقتطاع النص الذي يتجاوز 10000 رمز تلقائيًا.',
|
||||
qaTip: 'عند استخدام بيانات الأسئلة والأجوبة المهيكلة، يمكنك إنشاء مستندات تقرن الأسئلة بالأجوبة. يتم فهرسة هذه المستندات بناءً على جزء السؤال، مما يسمح للنظام باسترجاع الإجابات ذات الصلة بناءً على تشابه الاستعلام.',
|
||||
separator: 'محدد',
|
||||
separatorTip: 'المحدد هو الحرف المستخدم لفصل النص. \\n\\n و \\n هي محددات شائعة الاستخدام لفصل الفقرات والأسطر. جنبًا إلى جنب مع الفواصل (\\n\\n,\\n)، سيتم تقسيم الفقرات حسب الأسطر عند تجاوز الحد الأقصى لطول القطعة. يمكنك أيضًا استخدام محددات خاصة محددة بنفسك (مثل ***).',
|
||||
separatorPlaceholder: '\\n\\n للفقرات؛ \\n للأسطر',
|
||||
maxLength: 'أقصى طول للقطعة',
|
||||
maxLengthCheck: 'يجب أن يكون أقصى طول للقطعة أقل من {{limit}}',
|
||||
overlap: 'تداخل القطعة',
|
||||
overlapTip: 'يمكن أن يؤدي تعيين تداخل القطعة إلى الحفاظ على الصلة الدلالية بينها، مما يعزز تأثير الاسترجاع. يوصى بتعيين 10٪ -25٪ من الحد الأقصى لحجم القطعة.',
|
||||
overlapCheck: 'يجب ألا يكون تداخل القطعة أكبر من أقصى طول للقطعة',
|
||||
rules: 'قواعد المعالجة المسبقة للنص',
|
||||
removeExtraSpaces: 'استبدال المسافات المتتالية والأسطر الجديدة وعلامات الجدولة',
|
||||
removeUrlEmails: 'حذف جميع عناوين URL وعناوين البريد الإلكتروني',
|
||||
removeStopwords: 'إزالة كلمات التوقف مثل "a", "an", "the"',
|
||||
preview: 'معاينة',
|
||||
previewChunk: 'معاينة القطعة',
|
||||
reset: 'إعادة تعيين',
|
||||
indexMode: 'طريقة الفهرسة',
|
||||
qualified: 'عالية الجودة',
|
||||
highQualityTip: 'بمجرد الانتهاء من التضمين في وضع الجودة العالية، لا يتوفر الرجوع إلى الوضع الاقتصادي.',
|
||||
recommend: 'نوصي',
|
||||
qualifiedTip: 'يساعد استدعاء نموذج التضمين لمعالجة المستندات من أجل استرجاع أكثر دقة LLM على إنشاء إجابات عالية الجودة.',
|
||||
warning: 'يرجى إعداد مفتاح API لمزود النموذج أولاً.',
|
||||
click: 'الذهاب إلى الإعدادات',
|
||||
economical: 'اقتصادي',
|
||||
economicalTip: 'استخدام 10 كلمات رئيسية لكل قطعة للاسترجاع، لا يتم استهلاك أي رموز على حساب تقليل دقة الاسترجاع.',
|
||||
QATitle: 'التقسيم بتنسيق سؤال وجواب',
|
||||
QATip: 'سيؤدي تمكين هذا الخيار إلى استهلاك المزيد من الرموز',
|
||||
QALanguage: 'التقسيم باستخدام',
|
||||
useQALanguage: 'تقطيع بتنسيق سؤال وجواب في',
|
||||
estimateCost: 'تقدير',
|
||||
estimateSegment: 'القطع المقدرة',
|
||||
segmentCount: 'قطع',
|
||||
calculating: 'جارٍ الحساب...',
|
||||
fileSource: 'معالجة المستندات مسبقًا',
|
||||
notionSource: 'معالجة الصفحات مسبقًا',
|
||||
websiteSource: 'معالجة الموقع مسبقًا',
|
||||
other: 'وغيرها ',
|
||||
fileUnit: ' ملفات',
|
||||
notionUnit: ' صفحات',
|
||||
webpageUnit: ' صفحات',
|
||||
previousStep: 'الخطوة السابقة',
|
||||
nextStep: 'حفظ ومعالجة',
|
||||
save: 'حفظ ومعالجة',
|
||||
cancel: 'إلغاء',
|
||||
sideTipTitle: 'لماذا التقطيع والمعالجة المسبقة؟',
|
||||
sideTipP1: 'عند معالجة البيانات النصية، يعد التقطيع والتنظيف خطوتين مهمتين للمعالجة المسبقة.',
|
||||
sideTipP2: 'يقسم التقسيم النص الطويل إلى فقرات حتى تتمكن النماذج من فهمه بشكل أفضل. هذا يحسن جودة وصلة نتائج النموذج.',
|
||||
sideTipP3: 'يزيل التنظيف الأحرف والتنسيقات غير الضرورية، مما يجعل المعرفة أنظف وأسهل في التحليل.',
|
||||
sideTipP4: 'يؤدي التقطيع والتنظيف السليمتان إلى تحسين أداء النموذج، مما يوفر نتائج أكثر دقة وقيمة.',
|
||||
previewTitle: 'معاينة',
|
||||
previewTitleButton: 'معاينة',
|
||||
previewButton: 'التبديل إلى تنسيق سؤال وجواب',
|
||||
previewSwitchTipStart: 'معاينة القطعة الحالية بتنسيق نصي، وسيؤدي التبديل إلى معاينة تنسيق سؤال وجواب إلى',
|
||||
previewSwitchTipEnd: ' استهلاك رموز إضافية',
|
||||
characters: 'أحرف',
|
||||
indexSettingTip: 'لتغيير طريقة الفهرسة ونموذج التضمين، يرجى الانتقال إلى ',
|
||||
retrievalSettingTip: 'لتغيير إعداد الاسترجاع، يرجى الانتقال إلى ',
|
||||
datasetSettingLink: 'إعدادات المعرفة.',
|
||||
previewChunkTip: 'انقر فوق زر "معاينة القطعة" على اليسار لتحميل المعاينة',
|
||||
previewChunkCount: '{{count}} قطعة مقدرة',
|
||||
switch: 'تبديل',
|
||||
qaSwitchHighQualityTipTitle: 'يتطلب تنسيق سؤال وجواب طريقة فهرسة عالية الجودة',
|
||||
qaSwitchHighQualityTipContent: 'حاليا، تدعم طريقة الفهرسة عالية الجودة فقط تقطيع تنسيق سؤال وجواب. هل ترغب في التبديل إلى وضع الجودة العالية؟',
|
||||
notAvailableForParentChild: 'غير متاح لفهرس الأصل والطفل',
|
||||
notAvailableForQA: 'غير متاح لفهرس الأسئلة والأجوبة',
|
||||
parentChildDelimiterTip: 'المحدد هو الحرف المستخدم لفصل النص. يوصى باستخدام \\n\\n لتقسيم المستند الأصلي إلى قطع أصلية كبيرة. يمكنك أيضًا استخدام محددات خاصة محددة بنفسك.',
|
||||
parentChildChunkDelimiterTip: 'المحدد هو الحرف المستخدم لفصل النص. يوصى باستخدام \\n لتقسيم القطع الأصلية إلى قطع فرعية صغيرة. يمكنك أيضًا استخدام محددات خاصة محددة بنفسك.',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 تم إنشاء المعرفة',
|
||||
creationContent: 'قمنا بتسمية المعرفة تلقائيًا، يمكنك تعديلها في أي وقت.',
|
||||
label: 'اسم المعرفة',
|
||||
additionTitle: '🎉 تم تحميل المستند',
|
||||
additionP1: 'تم تحميل المستند إلى المعرفة',
|
||||
additionP2: '، يمكنك العثور عليه في قائمة مستندات المعرفة.',
|
||||
stop: 'إيقاف المعالجة',
|
||||
resume: 'استئناف المعالجة',
|
||||
navTo: 'الذهاب إلى المستند',
|
||||
sideTipTitle: 'ما التالي',
|
||||
sideTipContent: 'بعد الانتهاء من فهرسة المستندات، يمكنك إدارة المستندات وتعديلها، وتشغيل اختبارات الاسترجاع، وتعديل إعدادات المعرفة. يمكن بعد ذلك دمج المعرفة في تطبيقك كسياق، لذا تأكد من ضبط إعداد الاسترجاع لضمان الأداء الأمثل.',
|
||||
modelTitle: 'هل أنت متأكد من إيقاف التضمين؟',
|
||||
modelContent: 'إذا كنت بحاجة إلى استئناف المعالجة لاحقًا، فستستمر من حيث توقفت.',
|
||||
modelButtonConfirm: 'تأكيد',
|
||||
modelButtonCancel: 'إلغاء',
|
||||
},
|
||||
otherDataSource: {
|
||||
title: 'الاتصال بمصادر بيانات أخرى؟',
|
||||
description: 'حاليًا، تحتوي قاعدة معرفة Dify فقط على مصادر بيانات محدودة. تعد المساهمة بمصدر بيانات في قاعدة معرفة Dify طريقة رائعة للمساعدة في تعزيز مرونة النظام الأساسي وقوته لجميع المستخدمين. دليل المساهمة الخاص بنا يسهل البدء. يرجى النقر على الرابط أدناه لمعرفة المزيد.',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
const translation = {
|
||||
list: {
|
||||
title: 'المستندات',
|
||||
desc: 'يتم عرض جميع ملفات المعرفة هنا، ويمكن ربط المعرفة بأكملها باقتباسات Dify أو فهرستها عبر مكون الدردشة الإضافي.',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
addFile: 'إضافة ملف',
|
||||
addPages: 'إضافة صفحات',
|
||||
addUrl: 'إضافة عنوان URL',
|
||||
table: {
|
||||
header: {
|
||||
fileName: 'الاسم',
|
||||
chunkingMode: 'وضع التقطيع',
|
||||
words: 'الكلمات',
|
||||
hitCount: 'عدد الاسترجاع',
|
||||
uploadTime: 'وقت التحميل',
|
||||
status: 'الحالة',
|
||||
action: 'إجراء',
|
||||
},
|
||||
rename: 'إعادة تسمية',
|
||||
name: 'الاسم',
|
||||
},
|
||||
action: {
|
||||
uploadFile: 'تحميل ملف جديد',
|
||||
settings: 'إعدادات التقطيع',
|
||||
addButton: 'إضافة قطعة',
|
||||
add: 'إضافة قطعة',
|
||||
batchAdd: 'إضافة دفعة',
|
||||
archive: 'أرشيف',
|
||||
unarchive: 'إلغاء الأرشفة',
|
||||
delete: 'حذف',
|
||||
enableWarning: 'لا يمكن تمكين الملف المؤرشف',
|
||||
sync: 'مزامنة',
|
||||
pause: 'إيقاف مؤقت',
|
||||
resume: 'استئناف',
|
||||
},
|
||||
index: {
|
||||
enable: 'تمكين',
|
||||
disable: 'تعطيل',
|
||||
all: 'الكل',
|
||||
enableTip: 'يمكن فهرسة الملف',
|
||||
disableTip: 'لا يمكن فهرسة الملف',
|
||||
},
|
||||
sort: {
|
||||
uploadTime: 'وقت التحميل',
|
||||
hitCount: 'عدد الاسترجاع',
|
||||
},
|
||||
status: {
|
||||
queuing: 'في الانتظار',
|
||||
indexing: 'فهرسة',
|
||||
paused: 'متوقف مؤقتًا',
|
||||
error: 'خطأ',
|
||||
available: 'متاح',
|
||||
enabled: 'ممكن',
|
||||
disabled: 'معطل',
|
||||
archived: 'مؤرشف',
|
||||
},
|
||||
empty: {
|
||||
title: 'لا يوجد وثائق بعد',
|
||||
upload: {
|
||||
tip: 'يمكنك تحميل الملفات، والمزامنة من الموقع، أو من تطبيقات الويب مثل Notion و GitHub، إلخ.',
|
||||
},
|
||||
sync: {
|
||||
tip: 'سيقوم Dify بتنزيل الملفات بشكل دوري من Notion وإكمال المعالجة.',
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
title: 'هل أنت متأكد من الحذف؟',
|
||||
content: 'إذا كنت بحاجة إلى استئناف المعالجة لاحقًا، فستستمر من حيث توقفت',
|
||||
},
|
||||
batchModal: {
|
||||
title: 'إضافة قطع دفعة واحدة',
|
||||
csvUploadTitle: 'اسحب وأفلت ملف CSV هنا، أو ',
|
||||
browse: 'تصفح',
|
||||
tip: 'يجب أن يتوافق ملف CSV مع الهيكل التالي:',
|
||||
question: 'سؤال',
|
||||
answer: 'إجابة',
|
||||
contentTitle: 'محتوى القطعة',
|
||||
content: 'محتوى',
|
||||
template: 'قم بتنزيل القالب هنا',
|
||||
cancel: 'إلغاء',
|
||||
run: 'تشغيل الدفعة',
|
||||
runError: 'فشل تشغيل الدفعة',
|
||||
processing: 'في معالجة الدفعة',
|
||||
completed: 'اكتمل الاستيراد',
|
||||
error: 'خطأ في الاستيراد',
|
||||
ok: 'موافق',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
title: 'البيانات الوصفية',
|
||||
desc: 'يسمح تصنيف البيانات الوصفية للمستندات للذكاء الاصطناعي بالوصول إليها في الوقت المناسب ويكشف مصدر المراجع للمستخدمين.',
|
||||
dateTimeFormat: 'MMMM D, YYYY hh:mm A',
|
||||
docTypeSelectTitle: 'يرجى تحديد نوع المستند',
|
||||
docTypeChangeTitle: 'تغيير نوع المستند',
|
||||
docTypeSelectWarning:
|
||||
'إذا تم تغيير نوع المستند، فلن يتم الاحتفاظ بالبيانات الوصفية المملوءة الآن',
|
||||
firstMetaAction: 'هيا بنا',
|
||||
placeholder: {
|
||||
add: 'إضافة ',
|
||||
select: 'تحديد ',
|
||||
},
|
||||
source: {
|
||||
upload_file: 'تحميل الملف',
|
||||
notion: 'مزامنة من Notion',
|
||||
github: 'مزامنة من Github',
|
||||
local_file: 'ملف محلي',
|
||||
website_crawl: 'زحف الموقع',
|
||||
online_document: 'مستند عبر الإنترنت',
|
||||
},
|
||||
type: {
|
||||
book: 'كتاب',
|
||||
webPage: 'صفحة ويب',
|
||||
paper: 'ورقة بحثية',
|
||||
socialMediaPost: 'منشور وسائل التواصل الاجتماعي',
|
||||
personalDocument: 'مستند شخصي',
|
||||
businessDocument: 'مستند أعمال',
|
||||
IMChat: 'دردشة فورية',
|
||||
wikipediaEntry: 'إدخال ويكيبيديا',
|
||||
notion: 'مزامنة من Notion',
|
||||
github: 'مزامنة من Github',
|
||||
technicalParameters: 'المعلمات الفنية',
|
||||
},
|
||||
field: {
|
||||
processRule: {
|
||||
processDoc: 'معالجة المستند',
|
||||
segmentRule: 'قاعدة القطع',
|
||||
segmentLength: 'طول القطع',
|
||||
processClean: 'تنظيف عملية النص',
|
||||
},
|
||||
book: {
|
||||
title: 'العنوان',
|
||||
language: 'اللغة',
|
||||
author: 'المؤلف',
|
||||
publisher: 'الناشر',
|
||||
publicationDate: 'تاريخ النشر',
|
||||
ISBN: 'ISBN',
|
||||
category: 'الفئة',
|
||||
},
|
||||
webPage: {
|
||||
title: 'العنوان',
|
||||
url: 'عنوان URL',
|
||||
language: 'اللغة',
|
||||
authorPublisher: 'المؤلف/الناشر',
|
||||
publishDate: 'تاريخ النشر',
|
||||
topicKeywords: 'الموضوع/الكلمات الرئيسية',
|
||||
description: 'الوصف',
|
||||
},
|
||||
paper: {
|
||||
title: 'العنوان',
|
||||
language: 'اللغة',
|
||||
author: 'المؤلف',
|
||||
publishDate: 'تاريخ النشر',
|
||||
journalConferenceName: 'اسم المجلة/المؤتمر',
|
||||
volumeIssuePage: 'المجلد/العدد/الصفحة',
|
||||
DOI: 'DOI',
|
||||
topicsKeywords: 'المواضيع/الكلمات الرئيسية',
|
||||
abstract: 'الملخص',
|
||||
},
|
||||
socialMediaPost: {
|
||||
platform: 'المنصة',
|
||||
authorUsername: 'المؤلف/اسم المستخدم',
|
||||
publishDate: 'تاريخ النشر',
|
||||
postURL: 'عنوان URL للمنشور',
|
||||
topicsTags: 'المواضيع/العلامات',
|
||||
},
|
||||
personalDocument: {
|
||||
title: 'العنوان',
|
||||
author: 'المؤلف',
|
||||
creationDate: 'تاريخ الإنشاء',
|
||||
lastModifiedDate: 'تاريخ آخر تعديل',
|
||||
documentType: 'نوع المستند',
|
||||
tagsCategory: 'العلامات/الفئة',
|
||||
},
|
||||
businessDocument: {
|
||||
title: 'العنوان',
|
||||
author: 'المؤلف',
|
||||
creationDate: 'تاريخ الإنشاء',
|
||||
lastModifiedDate: 'تاريخ آخر تعديل',
|
||||
documentType: 'نوع المستند',
|
||||
departmentTeam: 'القسم/الفريق',
|
||||
},
|
||||
IMChat: {
|
||||
chatPlatform: 'منصة الدردشة',
|
||||
chatPartiesGroupName: 'أطراف الدردشة/اسم المجموعة',
|
||||
participants: 'المشاركون',
|
||||
startDate: 'تاريخ البدء',
|
||||
endDate: 'تاريخ الانتهاء',
|
||||
topicsKeywords: 'المواضيع/الكلمات الرئيسية',
|
||||
fileType: 'نوع الملف',
|
||||
},
|
||||
wikipediaEntry: {
|
||||
title: 'العنوان',
|
||||
language: 'اللغة',
|
||||
webpageURL: 'عنوان URL لصفحة الويب',
|
||||
editorContributor: 'المحرر/المساهم',
|
||||
lastEditDate: 'تاريخ آخر تعديل',
|
||||
summaryIntroduction: 'الملخص/المقدمة',
|
||||
},
|
||||
notion: {
|
||||
title: 'العنوان',
|
||||
language: 'اللغة',
|
||||
author: 'المؤلف',
|
||||
createdTime: 'وقت الإنشاء',
|
||||
lastModifiedTime: 'وقت آخر تعديل',
|
||||
url: 'عنوان URL',
|
||||
tag: 'العلامة',
|
||||
description: 'الوصف',
|
||||
},
|
||||
github: {
|
||||
repoName: 'اسم المستودع',
|
||||
repoDesc: 'وصف المستودع',
|
||||
repoOwner: 'مالك المستودع',
|
||||
fileName: 'اسم الملف',
|
||||
filePath: 'مسار الملف',
|
||||
programmingLang: 'لغة البرمجة',
|
||||
url: 'عنوان URL',
|
||||
license: 'الرخصة',
|
||||
lastCommitTime: 'وقت آخر التزام',
|
||||
lastCommitAuthor: 'مؤلف آخر التزام',
|
||||
},
|
||||
originInfo: {
|
||||
originalFilename: 'اسم الملف الأصلي',
|
||||
originalFileSize: 'حجم الملف الأصلي',
|
||||
uploadDate: 'تاريخ التحميل',
|
||||
lastUpdateDate: 'تاريخ آخر تحديث',
|
||||
source: 'المصدر',
|
||||
},
|
||||
technicalParameters: {
|
||||
segmentSpecification: 'مواصفات القطع',
|
||||
segmentLength: 'طول القطع',
|
||||
avgParagraphLength: 'متوسط طول الفقرة',
|
||||
paragraphs: 'الفقرات',
|
||||
hitCount: 'عدد الاسترجاع',
|
||||
embeddingTime: 'وقت التضمين',
|
||||
embeddedSpend: 'إنفاق التضمين',
|
||||
},
|
||||
},
|
||||
languageMap: {
|
||||
zh: 'صيني',
|
||||
en: 'إنجليزي',
|
||||
es: 'إسباني',
|
||||
fr: 'فرنسي',
|
||||
de: 'ألماني',
|
||||
ja: 'ياباني',
|
||||
ko: 'كوري',
|
||||
ru: 'روسي',
|
||||
ar: 'عربي',
|
||||
pt: 'برتغالي',
|
||||
it: 'إيطالي',
|
||||
nl: 'هولندي',
|
||||
pl: 'بولندي',
|
||||
sv: 'سويدي',
|
||||
tr: 'تركي',
|
||||
he: 'عبري',
|
||||
hi: 'هندي',
|
||||
da: 'دنماركي',
|
||||
fi: 'فنلندي',
|
||||
no: 'نرويجي',
|
||||
hu: 'مجري',
|
||||
el: 'يوناني',
|
||||
cs: 'تشيكي',
|
||||
th: 'تايلاندي',
|
||||
id: 'إندونيسي',
|
||||
},
|
||||
categoryMap: {
|
||||
book: {
|
||||
fiction: 'خيال',
|
||||
biography: 'سيرة شخصية',
|
||||
history: 'تاريخ',
|
||||
science: 'علوم',
|
||||
technology: 'تكنولوجيا',
|
||||
education: 'تعليم',
|
||||
philosophy: 'فلسفة',
|
||||
religion: 'دين',
|
||||
socialSciences: 'علوم اجتماعية',
|
||||
art: 'فن',
|
||||
travel: 'سفر',
|
||||
health: 'صحة',
|
||||
selfHelp: 'تطوير الذات',
|
||||
businessEconomics: 'أعمال واقتصاد',
|
||||
cooking: 'طبخ',
|
||||
childrenYoungAdults: 'أطفال وشباب',
|
||||
comicsGraphicNovels: 'قصص مصورة وروايات مصورة',
|
||||
poetry: 'شعر',
|
||||
drama: 'دراما',
|
||||
other: 'أخرى',
|
||||
},
|
||||
personalDoc: {
|
||||
notes: 'ملاحظات',
|
||||
blogDraft: 'مسودة مدونة',
|
||||
diary: 'مذكرات',
|
||||
researchReport: 'تقرير بحث',
|
||||
bookExcerpt: 'مقتطف من كتاب',
|
||||
schedule: 'جدول',
|
||||
list: 'قائمة',
|
||||
projectOverview: 'نظرة عامة على المشروع',
|
||||
photoCollection: 'مجموعة صور',
|
||||
creativeWriting: 'كتابة إبداعية',
|
||||
codeSnippet: 'مقتطف كود',
|
||||
designDraft: 'مسودة تصميم',
|
||||
personalResume: 'سيرة ذاتية شخصية',
|
||||
other: 'أخرى',
|
||||
},
|
||||
businessDoc: {
|
||||
meetingMinutes: 'محضر اجتماع',
|
||||
researchReport: 'تقرير بحث',
|
||||
proposal: 'اقتراح',
|
||||
employeeHandbook: 'دليل الموظف',
|
||||
trainingMaterials: 'مواد تدريبية',
|
||||
requirementsDocument: 'وثيقة المتطلبات',
|
||||
designDocument: 'وثيقة التصميم',
|
||||
productSpecification: 'مواصفات المنتج',
|
||||
financialReport: 'تقرير مالي',
|
||||
marketAnalysis: 'تحليل السوق',
|
||||
projectPlan: 'خطة المشروع',
|
||||
teamStructure: 'هيكل الفريق',
|
||||
policiesProcedures: 'السياسات والإجراءات',
|
||||
contractsAgreements: 'العقود والاتفاقيات',
|
||||
emailCorrespondence: 'مراسلات البريد الإلكتروني',
|
||||
other: 'أخرى',
|
||||
},
|
||||
},
|
||||
},
|
||||
embedding: {
|
||||
waiting: 'انتظار التضمين...',
|
||||
processing: 'معالجة التضمين...',
|
||||
paused: 'تم إيقاف التضمين مؤقتًا',
|
||||
completed: 'اكتمل التضمين',
|
||||
error: 'خطأ في التضمين',
|
||||
docName: 'مستند المعالجة المسبقة',
|
||||
mode: 'إعداد التقطيع',
|
||||
segmentLength: 'أقصى طول للقطعة',
|
||||
textCleaning: 'قواعد المعالجة المسبقة للنص',
|
||||
segments: 'الفقرات',
|
||||
highQuality: 'وضع عالي الجودة',
|
||||
economy: 'الوضع الاقتصادي',
|
||||
estimate: 'الاستهلاك المقدر',
|
||||
stop: 'إيقاف المعالجة',
|
||||
pause: 'إيقاف مؤقت',
|
||||
resume: 'استئناف',
|
||||
automatic: 'تلقائي',
|
||||
custom: 'مخصص',
|
||||
hierarchical: 'الأصل والطفل',
|
||||
previewTip: 'ستتوفر معاينة الفقرة بعد اكتمال التضمين',
|
||||
parentMaxTokens: 'الأصل',
|
||||
childMaxTokens: 'الطفل',
|
||||
},
|
||||
segment: {
|
||||
paragraphs: 'الفقرات',
|
||||
chunks_one: 'قطعة',
|
||||
chunks_other: 'قطع',
|
||||
parentChunks_one: 'قطعة أصلية',
|
||||
parentChunks_other: 'قطع أصلية',
|
||||
childChunks_one: 'قطعة فرعية',
|
||||
childChunks_other: 'قطع فرعية',
|
||||
searchResults_zero: 'نتيجة',
|
||||
searchResults_one: 'نتيجة',
|
||||
searchResults_other: 'نتائج',
|
||||
empty: 'لم يتم العثور على أي قطعة',
|
||||
clearFilter: 'مسح التصفية',
|
||||
chunk: 'قطعة',
|
||||
parentChunk: 'قطعة أصلية',
|
||||
newChunk: 'قطعة جديدة',
|
||||
childChunk: 'قطعة فرعية',
|
||||
newChildChunk: 'قطعة فرعية جديدة',
|
||||
keywords: 'كلمات رئيسية',
|
||||
addKeyWord: 'إضافة كلمة رئيسية',
|
||||
keywordEmpty: 'لا يمكن أن تكون الكلمة الرئيسية فارغة',
|
||||
keywordError: 'الحد الأقصى لطول الكلمة الرئيسية هو 20',
|
||||
keywordDuplicate: 'الكلمة الرئيسية موجودة بالفعل',
|
||||
characters_one: 'حرف',
|
||||
characters_other: 'أحرف',
|
||||
hitCount: 'عدد الاسترجاع',
|
||||
vectorHash: 'تجزئة المتجه: ',
|
||||
questionPlaceholder: 'أضف السؤال هنا',
|
||||
questionEmpty: 'لا يمكن أن يكون السؤال فارغًا',
|
||||
answerPlaceholder: 'أضف الإجابة هنا',
|
||||
answerEmpty: 'لا يمكن أن تكون الإجابة فارغة',
|
||||
contentPlaceholder: 'أضف المحتوى هنا',
|
||||
contentEmpty: 'لا يمكن أن يكون المحتوى فارغًا',
|
||||
newTextSegment: 'قطعة نصية جديدة',
|
||||
newQaSegment: 'قطعة سؤال وجواب جديدة',
|
||||
addChunk: 'إضافة قطعة',
|
||||
addChildChunk: 'إضافة قطعة فرعية',
|
||||
addAnother: 'إضافة أخرى',
|
||||
delete: 'حذف هذه القطعة؟',
|
||||
chunkAdded: 'تم إضافة قطعة واحدة',
|
||||
childChunkAdded: 'تم إضافة قطعة فرعية واحدة',
|
||||
editChunk: 'تعديل القطعة',
|
||||
editParentChunk: 'تعديل القطعة الأصلية',
|
||||
editChildChunk: 'تعديل القطعة الفرعية',
|
||||
chunkDetail: 'تفاصيل القطعة',
|
||||
regenerationConfirmTitle: 'هل تريد إعادة إنشاء القطع الفرعية؟',
|
||||
regenerationConfirmMessage: 'سوف تؤدي إعادة إنشاء القطع الفرعية إلى استبدال القطع الفرعية الحالية، بما في ذلك القطع المعدلة والقطع المضافة حديثًا. لا يمكن التراجع عن إعادة الإنشاء.',
|
||||
regeneratingTitle: 'إعادة إنشاء القطع الفرعية',
|
||||
regeneratingMessage: 'قد يستغرق هذا لحظة، يرجى الانتظار...',
|
||||
regenerationSuccessTitle: 'اكتملت إعادة الإنشاء',
|
||||
regenerationSuccessMessage: 'يمكنك إغلاق هذه النافذة.',
|
||||
edited: 'معدل',
|
||||
editedAt: 'تم التعديل في',
|
||||
dateTimeFormat: 'MM/DD/YYYY h:mm',
|
||||
expandChunks: 'توسيع القطع',
|
||||
collapseChunks: 'طي القطع',
|
||||
allFilesUploaded: 'يجب تحميل جميع الملفات قبل الحفظ',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
const translation = {
|
||||
title: 'اختبار الاسترجاع',
|
||||
settingTitle: 'إعداد الاسترجاع',
|
||||
desc: 'اختبار تأثير مطابقة المعرفة بناءً على نص الاستعلام المقدم.',
|
||||
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
|
||||
records: 'سجلات',
|
||||
table: {
|
||||
header: {
|
||||
source: 'المصدر',
|
||||
time: 'وقت',
|
||||
queryContent: 'محتوى الاستعلام',
|
||||
},
|
||||
},
|
||||
input: {
|
||||
title: 'النص المصدر',
|
||||
placeholder: 'يرجى إدخال نص، ويوصى بجملة تعريفية قصيرة.',
|
||||
countWarning: 'ما يصل إلى 200 حرف.',
|
||||
indexWarning: 'معرفة عالية الجودة فقط.',
|
||||
testing: 'اختبار',
|
||||
},
|
||||
hit: {
|
||||
title: '{{num}} قطع مسترجعة',
|
||||
emptyTip: 'ستظهر نتائج اختبار الاسترجاع هنا',
|
||||
},
|
||||
noRecentTip: 'لا توجد نتائج استعلام حديثة هنا',
|
||||
viewChart: 'عرض مخطط VECTOR',
|
||||
viewDetail: 'عرض التفاصيل',
|
||||
chunkDetail: 'تفاصيل المقطع',
|
||||
hitChunks: 'إصابة {{num}} مقاطع فرعية',
|
||||
open: 'فتح',
|
||||
keyword: 'الكلمات الرئيسية',
|
||||
imageUploader: {
|
||||
tip: 'قم بتحميل الصور أو إسقاطها (الحد الأقصى {{batchCount}}، {{size}} ميغابايت لكل صورة)',
|
||||
tooltip: 'رفع الصور (الحد الأقصى {{batchCount}}، {{size}} ميغابايت لكل صورة)',
|
||||
dropZoneTip: 'اسحب الملف هنا للتحميل',
|
||||
singleChunkAttachmentLimitTooltip: 'لا يمكن أن يتجاوز عدد المرفقات ذات القطعة الواحدة {{limit}}',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
const translation = {
|
||||
creation: {
|
||||
backToKnowledge: 'العودة إلى المعرفة',
|
||||
createFromScratch: {
|
||||
title: 'سير عمل معرفة فارغ',
|
||||
description: 'إنشاء سير عمل مخصص من الصفر مع التحكم الكامل في معالجة البيانات وهيكلها.',
|
||||
},
|
||||
importDSL: 'استيراد من ملف DSL',
|
||||
createKnowledge: 'إنشاء المعرفة',
|
||||
errorTip: 'فشل إنشاء قاعدة المعرفة',
|
||||
successTip: 'تم إنشاء قاعدة المعرفة بنجاح',
|
||||
caution: 'تنبيه',
|
||||
},
|
||||
templates: {
|
||||
customized: 'مخصص',
|
||||
},
|
||||
operations: {
|
||||
choose: 'اختر',
|
||||
details: 'التفاصيل',
|
||||
editInfo: 'تعديل المعلومات',
|
||||
useTemplate: 'استخدام سير عمل المعرفة هذا',
|
||||
backToDataSource: 'العودة إلى مصدر البيانات',
|
||||
process: 'معالجة',
|
||||
dataSource: 'مصدر البيانات',
|
||||
saveAndProcess: 'حفظ ومعالجة',
|
||||
preview: 'معاينة',
|
||||
exportPipeline: 'تصدير سير العمل',
|
||||
convert: 'تحويل',
|
||||
},
|
||||
knowledgeNameAndIcon: 'اسم وأيقونة المعرفة',
|
||||
knowledgeNameAndIconPlaceholder: 'يرجى إدخال اسم قاعدة المعرفة',
|
||||
knowledgeDescription: 'وصف المعرفة',
|
||||
knowledgeDescriptionPlaceholder: 'صف ما يوجد في قاعدة المعرفة هذه. يسمح الوصف التفصيلي للذكاء الاصطناعي بالوصول إلى محتوى مجموعة البيانات بشكل أكثر دقة. إذا كان فارغًا، فسيستخدم Dify استراتيجية المطابقة الافتراضية. (اختياري)',
|
||||
knowledgePermissions: 'أذونات',
|
||||
editPipelineInfo: 'تعديل معلومات سير العمل',
|
||||
pipelineNameAndIcon: 'اسم وأيقونة سير العمل',
|
||||
deletePipeline: {
|
||||
title: 'هل أنت متأكد من حذف قالب سير العمل هذا؟',
|
||||
content: 'حذف قالب سير العمل لا رجعة فيه.',
|
||||
},
|
||||
publishPipeline: {
|
||||
success: {
|
||||
message: 'تم نشر سير عمل المعرفة',
|
||||
tip: '<CustomLink>الذهاب إلى المستندات</CustomLink> لإضافة أو إدارة المستندات.',
|
||||
},
|
||||
error: {
|
||||
message: 'فشل نشر سير عمل المعرفة',
|
||||
},
|
||||
},
|
||||
publishTemplate: {
|
||||
success: {
|
||||
message: 'تم نشر قالب سير العمل',
|
||||
tip: 'يمكنك استخدام هذا القالب في صفحة الإنشاء.',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
},
|
||||
error: {
|
||||
message: 'فشل نشر قالب سير العمل',
|
||||
},
|
||||
},
|
||||
exportDSL: {
|
||||
successTip: 'تم تصدير DSL لسير العمل بنجاح',
|
||||
errorTip: 'فشل تصدير DSL لسير العمل',
|
||||
},
|
||||
details: {
|
||||
createdBy: 'بواسطة {{author}}',
|
||||
structure: 'الهيكل',
|
||||
structureTooltip: 'يحدد هيكل القطعة كيفية تقسيم المستندات وفهرستها - تقديم أوضاع عامة، الأصل والطفل، والأسئلة والأجوبة - وهي فريدة لكل قاعدة معرفة.',
|
||||
},
|
||||
testRun: {
|
||||
title: 'تشغيل اختباري',
|
||||
tooltip: 'في وضع التشغيل الاختباري، يُسمح باستيراد مستند واحد فقط في كل مرة لسهولة التصحيح والملاحظة.',
|
||||
steps: {
|
||||
dataSource: 'مصدر البيانات',
|
||||
documentProcessing: 'معالجة المستندات',
|
||||
},
|
||||
dataSource: {
|
||||
localFiles: 'الملفات المحلية',
|
||||
},
|
||||
notion: {
|
||||
title: 'اختر صفحات Notion',
|
||||
docTitle: 'مستندات Notion',
|
||||
},
|
||||
},
|
||||
inputField: 'حقل الإدخال',
|
||||
inputFieldPanel: {
|
||||
title: 'حقول إدخال المستخدم',
|
||||
description: 'تُستخدم حقول إدخال المستخدم لتعريف وجمع المتغيرات المطلوبة أثناء عملية تنفيذ سير العمل. يمكن للمستخدمين تخصيص نوع الحقل وتكوين قيمة الإدخال بمرونة لتلبية احتياجات مصادر البيانات المختلفة أو خطوات معالجة المستندات.',
|
||||
uniqueInputs: {
|
||||
title: 'مدخلات فريدة لكل مدخل',
|
||||
tooltip: 'المدخلات الفريدة يمكن الوصول إليها فقط لمصدر البيانات المحدد وعقده النهائية. لن يحتاج المستخدمون إلى تعبئتها عند اختيار مصادر بيانات أخرى. ستظهر فقط حقول الإدخال المشار إليها بواسطة متغيرات مصدر البيانات في الخطوة الأولى (مصدر البيانات). ستظهر جميع الحقول الأخرى في الخطوة الثانية (معالجة المستندات).',
|
||||
},
|
||||
globalInputs: {
|
||||
title: 'مدخلات عالمية لجميع المداخل',
|
||||
tooltip: 'المدخلات العالمية مشتركة عبر جميع العقد. سيحتاج المستخدمون إلى تعبئتها عند اختيار أي مصدر بيانات. على سبيل المثال، يمكن تطبيق حقول مثل المحدد والحد الأقصى لطول القطعة بشكل موحد عبر مصادر بيانات متعددة. ستظهر فقط حقول الإدخال المشار إليها بواسطة متغيرات مصدر البيانات في الخطوة الأولى (مصدر البيانات). ستظهر جميع الحقول الأخرى في الخطوة الثانية (معالجة المستندات).',
|
||||
},
|
||||
addInputField: 'إضافة حقل إدخال',
|
||||
editInputField: 'تعديل حقل إدخال',
|
||||
preview: {
|
||||
stepOneTitle: 'مصدر البيانات',
|
||||
stepTwoTitle: 'معالجة المستندات',
|
||||
},
|
||||
error: {
|
||||
variableDuplicate: 'اسم المتغير موجود بالفعل. يرجى اختيار اسم مختلف.',
|
||||
},
|
||||
},
|
||||
addDocuments: {
|
||||
title: 'إضافة مستندات',
|
||||
steps: {
|
||||
chooseDatasource: 'اختر مصدر بيانات',
|
||||
processDocuments: 'معالجة المستندات',
|
||||
processingDocuments: 'جارٍ معالجة المستندات',
|
||||
},
|
||||
backToDataSource: 'مصدر البيانات',
|
||||
stepOne: {
|
||||
preview: 'معاينة',
|
||||
},
|
||||
stepTwo: {
|
||||
chunkSettings: 'إعدادات القطعة',
|
||||
previewChunks: 'معاينة القطع',
|
||||
},
|
||||
stepThree: {
|
||||
learnMore: 'تعرف على المزيد',
|
||||
},
|
||||
characters: 'أحرف',
|
||||
selectOnlineDocumentTip: 'معالجة ما يصل إلى {{count}} صفحة',
|
||||
selectOnlineDriveTip: 'معالجة ما يصل إلى {{count}} ملف، بحد أقصى {{fileSize}} ميجابايت لكل منها',
|
||||
},
|
||||
documentSettings: {
|
||||
title: 'إعدادات المستند',
|
||||
},
|
||||
onlineDocument: {
|
||||
pageSelectorTitle: '{{name}} صفحات',
|
||||
},
|
||||
onlineDrive: {
|
||||
notConnected: '{{name}} غير متصل',
|
||||
notConnectedTip: 'للمزامنة مع {{name}}، يجب إنشاء اتصال بـ {{name}} أولاً.',
|
||||
breadcrumbs: {
|
||||
allBuckets: 'جميع حاويات التخزين السحابية',
|
||||
allFiles: 'جميع الملفات',
|
||||
searchResult: 'العثور على {{searchResultsLength}} عناصر في مجلد "{{folderName}}"',
|
||||
searchPlaceholder: 'بحث في الملفات...',
|
||||
},
|
||||
notSupportedFileType: 'نوع الملف هذا غير مدعوم',
|
||||
emptyFolder: 'هذا المجلد فارغ',
|
||||
emptySearchResult: 'لم يتم العثور على أي عناصر',
|
||||
resetKeywords: 'إعادة تعيين الكلمات الرئيسية',
|
||||
},
|
||||
credentialSelector: {
|
||||
},
|
||||
configurationTip: 'تكوين {{pluginName}}',
|
||||
conversion: {
|
||||
title: 'التحويل إلى سير عمل المعرفة',
|
||||
descriptionChunk1: 'يمكنك الآن تحويل قاعدة المعرفة الحالية لاستخدام سير عمل المعرفة لمعالجة المستندات',
|
||||
descriptionChunk2: ' - نهج أكثر انفتاحًا ومرونة مع الوصول إلى الإضافات من سوقنا. سيطبق هذا طريقة المعالجة الجديدة على جميع المستندات المستقبلية.',
|
||||
warning: 'لا يمكن التراجع عن هذا الإجراء.',
|
||||
confirm: {
|
||||
title: 'تأكيد',
|
||||
content: 'هذا الإجراء دائم. لن تتمكن من العودة إلى الطريقة السابقة. يرجى التأكيد للتحويل.',
|
||||
},
|
||||
errorMessage: 'فشل تحويل مجموعة البيانات إلى سير عمل',
|
||||
successMessage: 'تم تحويل مجموعة البيانات إلى سير عمل بنجاح',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
const translation = {
|
||||
title: 'إعدادات المعرفة',
|
||||
desc: 'هنا يمكنك تعديل الخصائص وإعدادات الاسترجاع لهذه المعرفة.',
|
||||
form: {
|
||||
name: 'اسم المعرفة',
|
||||
nameAndIcon: 'الاسم والأيقونة',
|
||||
namePlaceholder: 'يرجى إدخال اسم المعرفة',
|
||||
nameError: 'لا يمكن أن يكون الاسم فارغًا',
|
||||
desc: 'الوصف',
|
||||
descInfo: 'يرجى كتابة وصف نصي واضح لتوضيح محتوى المعرفة. سيتم استخدام هذا الوصف كأساس للمطابقة عند الاختيار من بين معارف متعددة للاستنتاج.',
|
||||
descPlaceholder: 'صف ما يوجد في مجموعة البيانات هذه. يسمح الوصف التفصيلي للذكاء الاصطناعي بالوصول إلى محتوى مجموعة البيانات في الوقت المناسب. إذا كان فارغًا، فسيستخدم Dify استراتيجية المطابقة الافتراضية.',
|
||||
helpText: 'تعرف على كيفية كتابة وصف جيد لمجموعة البيانات.',
|
||||
descWrite: 'تعرف على كيفية كتابة وصف جيد للمعرفة.',
|
||||
permissions: 'أذونات',
|
||||
permissionsOnlyMe: 'أنا فقط',
|
||||
permissionsAllMember: 'جميع أعضاء الفريق',
|
||||
permissionsInvitedMembers: 'أعضاء الفريق الجزئيين',
|
||||
me: '(أنت)',
|
||||
onSearchResults: 'لا يوجد أعضاء يطابقون استعلام البحث الخاص بك.\nحاول البحث مرة أخرى.',
|
||||
chunkStructure: {
|
||||
title: 'هيكل القطعة',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
description: ' حول هيكل القطعة.',
|
||||
},
|
||||
indexMethod: 'طريقة الفهرسة',
|
||||
indexMethodHighQuality: 'جودة عالية',
|
||||
indexMethodHighQualityTip: 'يساعد استدعاء نموذج التضمين لمعالجة المستندات من أجل استرجاع أكثر دقة LLM على إنشاء إجابات عالية الجودة.',
|
||||
upgradeHighQualityTip: 'بمجرد الترقية إلى وضع الجودة العالية، لا يتوفر الرجوع إلى الوضع الاقتصادي',
|
||||
indexMethodEconomy: 'اقتصادي',
|
||||
indexMethodEconomyTip: 'استخدام {{count}} كلمات رئيسية لكل قطعة للاسترجاع، لا يتم استهلاك أي رموز على حساب دقة الاسترجاع المنخفضة.',
|
||||
numberOfKeywords: 'عدد الكلمات الرئيسية',
|
||||
embeddingModel: 'نموذج التضمين',
|
||||
embeddingModelTip: 'لتغيير النموذج المضمن، يرجى الانتقال إلى ',
|
||||
embeddingModelTipLink: 'الإعدادات',
|
||||
retrievalSetting: {
|
||||
title: 'إعداد الاسترجاع',
|
||||
method: 'طريقة الاسترجاع',
|
||||
learnMore: 'تعرف على المزيد',
|
||||
description: ' حول طريقة الاسترجاع.',
|
||||
longDescription: ' حول طريقة الاسترجاع، يمكنك تغيير هذا في أي وقت في إعدادات المعرفة.',
|
||||
multiModalTip: 'عندما يدعم نموذج التضمين متعدد الوسائط، يرجى اختيار نموذج إعادة ترتيب متعدد الوسائط للحصول على أداء أفضل.',
|
||||
},
|
||||
externalKnowledgeAPI: 'واجهة برمجة تطبيقات المعرفة الخارجية',
|
||||
externalKnowledgeID: 'معرف المعرفة الخارجية',
|
||||
retrievalSettings: 'إعدادات الاسترجاع',
|
||||
save: 'حفظ',
|
||||
indexMethodChangeToEconomyDisabledTip: 'غير متوفر للرجوع من الجودة العالية إلى الوضع الاقتصادي',
|
||||
searchModel: 'نموذج البحث',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
const translation = {
|
||||
knowledge: 'المعرفة',
|
||||
chunkingMode: {
|
||||
general: 'عام',
|
||||
parentChild: 'الأصل والطفل',
|
||||
qa: 'سؤال وجواب',
|
||||
graph: 'رسم بياني',
|
||||
},
|
||||
parentMode: {
|
||||
paragraph: 'فقرة',
|
||||
fullDoc: 'مستند كامل',
|
||||
},
|
||||
externalTag: 'خارجي',
|
||||
externalAPI: 'واجهة برمجة تطبيقات خارجية',
|
||||
externalAPIPanelTitle: 'واجهة برمجة تطبيقات المعرفة الخارجية',
|
||||
externalKnowledgeId: 'معرف المعرفة الخارجية',
|
||||
externalKnowledgeName: 'اسم المعرفة الخارجية',
|
||||
externalKnowledgeDescription: 'وصف المعرفة',
|
||||
externalKnowledgeIdPlaceholder: 'يرجى إدخال معرف المعرفة',
|
||||
externalKnowledgeNamePlaceholder: 'يرجى إدخال اسم قاعدة المعرفة',
|
||||
externalKnowledgeDescriptionPlaceholder: 'صف ما يوجد في قاعدة المعرفة هذه (اختياري)',
|
||||
learnHowToWriteGoodKnowledgeDescription: 'تعرف على كيفية كتابة وصف جيد للمعرفة',
|
||||
externalAPIPanelDescription: 'تُستخدم واجهة برمجة تطبيقات المعرفة الخارجية للاتصال بقاعدة معرفة خارج Dify واسترجاع المعرفة من قاعدة المعرفة تلك.',
|
||||
externalAPIPanelDocumentation: 'تعرف على كيفية إنشاء واجهة برمجة تطبيقات المعرفة الخارجية',
|
||||
externalKnowledgeBase: 'قاعدة المعرفة الخارجية',
|
||||
localDocs: 'مستندات محلية',
|
||||
documentCount: ' مستندات',
|
||||
docAllEnabled_one: '{{count}} مستند ممكن',
|
||||
docAllEnabled_other: 'تم تمكين جميع المستندات البالغ عددها {{count}}',
|
||||
partialEnabled_one: 'إجمالي {{count}} مستند، {{num}} متاح',
|
||||
partialEnabled_other: 'إجمالي {{count}} مستندات، {{num}} متاح',
|
||||
wordCount: ' ألف كلمة',
|
||||
appCount: ' تطبيقات مرتبطة',
|
||||
updated: 'محدث',
|
||||
createDataset: 'إنشاء المعرفة',
|
||||
createFromPipeline: 'إنشاء من سير عمل المعرفة',
|
||||
createNewExternalAPI: 'إنشاء واجهة برمجة تطبيقات معرفة خارجية جديدة',
|
||||
noExternalKnowledge: 'لا توجد واجهة برمجة تطبيقات معرفة خارجية حتى الآن، انقر هنا لإنشاء',
|
||||
createExternalAPI: 'إضافة واجهة برمجة تطبيقات معرفة خارجية',
|
||||
editExternalAPIFormTitle: 'تعديل واجهة برمجة تطبيقات المعرفة الخارجية',
|
||||
editExternalAPITooltipTitle: 'المعرفة المرتبطة',
|
||||
editExternalAPIConfirmWarningContent: {
|
||||
front: 'ترتبط واجهة برمجة تطبيقات المعرفة الخارجية هذه بـ',
|
||||
end: 'معرفة خارجية، وسيتم تطبيق هذا التعديل عليها جميعًا. هل أنت متأكد أنك تريد حفظ هذا التغيير؟',
|
||||
},
|
||||
editExternalAPIFormWarning: {
|
||||
front: 'ترتبط واجهة برمجة التطبيقات الخارجية هذه بـ',
|
||||
end: 'معرفة خارجية',
|
||||
},
|
||||
deleteExternalAPIConfirmWarningContent: {
|
||||
title: {
|
||||
front: 'حذف',
|
||||
end: '؟',
|
||||
},
|
||||
content: {
|
||||
front: 'ترتبط واجهة برمجة تطبيقات المعرفة الخارجية هذه بـ',
|
||||
end: 'معرفة خارجية. سيؤدي حذف واجهة برمجة التطبيقات هذه إلى إبطالها جميعًا. هل أنت متأكد أنك تريد حذف واجهة برمجة التطبيقات هذه؟',
|
||||
},
|
||||
noConnectionContent: 'هل أنت متأكد من حذف واجهة برمجة التطبيقات هذه؟',
|
||||
},
|
||||
selectExternalKnowledgeAPI: {
|
||||
placeholder: 'اختر واجهة برمجة تطبيقات معرفة خارجية',
|
||||
},
|
||||
connectDataset: 'الاتصال بقاعدة معرفة خارجية',
|
||||
connectDatasetIntro: {
|
||||
title: 'كيفية الاتصال بقاعدة معرفة خارجية',
|
||||
content: {
|
||||
front: 'للاتصال بقاعدة معرفة خارجية، تحتاج إلى إنشاء واجهة برمجة تطبيقات خارجية أولاً. يرجى القراءة بعناية والرجوع إلى',
|
||||
link: 'تعرف على كيفية إنشاء واجهة برمجة تطبيقات خارجية',
|
||||
end: '. ثم ابحث عن معرف المعرفة المقابل واملأه في النموذج على اليسار. إذا كانت جميع المعلومات صحيحة، فسيقفز تلقائيًا إلى اختبار الاسترجاع في قاعدة المعرفة بعد النقر فوق زر الاتصال.',
|
||||
},
|
||||
learnMore: 'تعرف على المزيد',
|
||||
},
|
||||
connectHelper: {
|
||||
helper1: 'تصل بقواعد المعرفة الخارجية عبر API ومعرف قاعدة المعرفة. حاليًا، ',
|
||||
helper2: 'يتم دعم وظيفة الاسترجاع فقط',
|
||||
helper3: '. نوصي بشدة أن تقوم بـ ',
|
||||
helper4: 'قراءة وثائق المساعدة',
|
||||
helper5: ' بعناية قبل استخدام هذه الميزة.',
|
||||
},
|
||||
createDatasetIntro: 'استيراد بيانات النص الخاصة بك أو كتابة البيانات في الوقت الفعلي عبر Webhook لتحسين سياق LLM.',
|
||||
deleteDatasetConfirmTitle: 'حذف هذه المعرفة؟',
|
||||
deleteDatasetConfirmContent:
|
||||
'حذف المعرفة لا رجعة فيه. لن يتمكن المستخدمون بعد الآن من الوصول إلى معرفتك، وسيتم حذف جميع تكوينات الموجه والسجلات بشكل دائم.',
|
||||
datasetUsedByApp: 'يتم استخدام المعرفة بواسطة بعض التطبيقات. لن تتمكن التطبيقات بعد الآن من استخدام هذه المعرفة، وسيتم حذف جميع تكوينات الموجه والسجلات بشكل دائم.',
|
||||
datasetDeleted: 'تم حذف المعرفة',
|
||||
datasetDeleteFailed: 'فشل حذف المعرفة',
|
||||
didYouKnow: 'هل تعلم؟',
|
||||
intro1: 'يمكن دمج المعرفة في تطبيق Dify ',
|
||||
intro2: 'كسياق',
|
||||
intro3: '،',
|
||||
intro4: 'أو ',
|
||||
intro5: 'يمكن نشرها',
|
||||
intro6: ' كخدمة مستقلة.',
|
||||
unavailable: 'غير متاح',
|
||||
datasets: 'المعرفة',
|
||||
datasetsApi: 'الوصول إلى API',
|
||||
externalKnowledgeForm: {
|
||||
connect: 'اتصال',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
externalAPIForm: {
|
||||
name: 'الاسم',
|
||||
endpoint: 'نقطة نهاية API',
|
||||
apiKey: 'مفتاح API',
|
||||
save: 'حفظ',
|
||||
cancel: 'إلغاء',
|
||||
edit: 'تعديل',
|
||||
encrypted: {
|
||||
front: 'سيتم تشفير رمز API الخاص بك وتخزينه باستخدام',
|
||||
end: 'تقنية.',
|
||||
},
|
||||
},
|
||||
retrieval: {
|
||||
semantic_search: {
|
||||
title: 'بحث المتجهات',
|
||||
description: 'إنشاء تضمينات الاستعلام والبحث عن قطعة النص الأكثر تشابهًا مع تمثيلها المتجه.',
|
||||
},
|
||||
full_text_search: {
|
||||
title: 'بحث النص الكامل',
|
||||
description: 'فهرسة جميع المصطلحات في المستند، مما يسمح للمستخدمين بالبحث عن أي مصطلح واسترجاع قطعة نصية ذات صلة تحتوي على تلك المصطلحات.',
|
||||
},
|
||||
hybrid_search: {
|
||||
title: 'بحث هجين',
|
||||
description: 'تنفيذ البحث بالنص الكامل والبحث المتجه في وقت واحد، وإعادة الترتيب لتحديد أفضل تطابق لاستعلام المستخدم. يمكن للمستخدمين اختيار تعيين الأوزان أو التكوين لنموذج إعادة الترتيب.',
|
||||
recommend: 'نوصي',
|
||||
},
|
||||
keyword_search: {
|
||||
title: 'فهرس معكوس',
|
||||
description: 'الفهرس المعكوس هو هيكل يستخدم للاسترجاع الفعال. منظم حسب المصطلحات، يشير كل مصطلح إلى المستندات أو صفحات الويب التي تحتوي عليه.',
|
||||
},
|
||||
change: 'تغيير',
|
||||
changeRetrievalMethod: 'تغيير طريقة الاسترجاع',
|
||||
},
|
||||
docsFailedNotice: 'فشل فهرسة المستندات',
|
||||
retry: 'إعادة المحاولة',
|
||||
documentsDisabled: '{{num}} مستندات معطلة - غير نشطة لأكثر من 30 يومًا',
|
||||
enable: 'تمكين',
|
||||
indexingTechnique: {
|
||||
high_quality: 'HQ',
|
||||
economy: 'ECO',
|
||||
},
|
||||
indexingMethod: {
|
||||
semantic_search: 'VECTOR',
|
||||
full_text_search: 'FULL TEXT',
|
||||
hybrid_search: 'HYBRID',
|
||||
invertedIndex: 'فهرس معكوس',
|
||||
},
|
||||
defaultRetrievalTip: 'يستخدم الاسترجاع متعدد المسارات افتراضيًا. يتم استرجاع المعرفة من قواعد معرفة متعددة ثم إعادة ترتيبها.',
|
||||
mixtureHighQualityAndEconomicTip: 'مطلوب نموذج إعادة الترتيب لخلط قواعد المعرفة عالية الجودة والاقتصادية.',
|
||||
inconsistentEmbeddingModelTip: 'مطلوب نموذج إعادة الترتيب إذا كانت نماذج التضمين لقواعد المعرفة المختارة غير متسقة.',
|
||||
mixtureInternalAndExternalTip: 'مطلوب نموذج إعادة الترتيب لخلط المعرفة الداخلية والخارجية.',
|
||||
allExternalTip: 'عند استخدام المعرفة الخارجية فقط، يمكن للمستخدم اختيار ما إذا كان سيمكن نموذج إعادة الترتيب. إذا لم يتم تمكينه، فسيتم فرز القطع المسترجعة بناءً على الدرجات. عندما تكون استراتيجيات الاسترجاع لقواعد المعرفة المختلفة غير متسقة، فستكون غير دقيقة.',
|
||||
retrievalSettings: 'إعداد الاسترجاع',
|
||||
rerankSettings: 'إعداد إعادة الترتيب',
|
||||
weightedScore: {
|
||||
title: 'الدرجة المرجحة',
|
||||
description: 'من خلال تعديل الأوزان المخصصة، تحدد استراتيجية إعادة الترتيب هذه ما إذا كانت الأولوية للمطابقة الدلالية أو الكلمات الرئيسية.',
|
||||
semanticFirst: 'الدلالي أولاً',
|
||||
keywordFirst: 'الكلمة الرئيسية أولاً',
|
||||
customized: 'مخصص',
|
||||
semantic: 'دلالي',
|
||||
keyword: 'كلمة رئيسية',
|
||||
},
|
||||
nTo1RetrievalLegacy: 'سيتم إيقاف الاسترجاع من N إلى 1 رسميًا اعتبارًا من سبتمبر. يوصى باستخدام أحدث استرجاع متعدد المسارات للحصول على نتائج أفضل. ',
|
||||
nTo1RetrievalLegacyLink: 'تعرف على المزيد',
|
||||
nTo1RetrievalLegacyLinkText: ' سيتم إيقاف الاسترجاع من N إلى 1 رسميًا في سبتمبر.',
|
||||
batchAction: {
|
||||
selected: 'محدد',
|
||||
enable: 'تمكين',
|
||||
disable: 'تعطيل',
|
||||
archive: 'أرشيف',
|
||||
delete: 'حذف',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
preprocessDocument: '{{num}} معالجة المستندات مسبقًا',
|
||||
allKnowledge: 'كل المعرفة',
|
||||
allKnowledgeDescription: 'حدد لعرض كل المعرفة في مساحة العمل هذه. يمكن لمالك مساحة العمل فقط إدارة كل المعرفة.',
|
||||
embeddingModelNotAvailable: 'نموذج التضمين غير متوفر.',
|
||||
metadata: {
|
||||
metadata: 'بيانات وصفية',
|
||||
addMetadata: 'إضافة بيانات وصفية',
|
||||
chooseTime: 'اختر وقتًا...',
|
||||
createMetadata: {
|
||||
title: 'بيانات وصفية جديدة',
|
||||
back: 'رجوع',
|
||||
type: 'نوع',
|
||||
name: 'الاسم',
|
||||
namePlaceholder: 'إضافة اسم البيانات الوصفية',
|
||||
},
|
||||
checkName: {
|
||||
empty: 'لا يمكن أن يكون اسم البيانات الوصفية فارغًا',
|
||||
invalid: 'يمكن أن يحتوي اسم البيانات الوصفية فقط على أحرف صغيرة وأرقام وشرطات سفلية ويجب أن يبدأ بحرف صغير',
|
||||
tooLong: 'لا يمكن أن يتجاوز اسم البيانات الوصفية {{max}} حرفًا',
|
||||
},
|
||||
batchEditMetadata: {
|
||||
editMetadata: 'تعديل البيانات الوصفية',
|
||||
editDocumentsNum: 'تعديل {{num}} مستندات',
|
||||
applyToAllSelectDocument: 'تطبيق على جميع المستندات المحددة',
|
||||
applyToAllSelectDocumentTip: 'إنشاء جميع البيانات الوصفية المعدلة والجديدة أعلاه تلقائيًا لجميع المستندات المحددة، وإلا فإن تعديل البيانات الوصفية سينطبق فقط على المستندات التي تحتوي عليها.',
|
||||
multipleValue: 'قيمة متعددة',
|
||||
},
|
||||
selectMetadata: {
|
||||
search: 'بحث في البيانات الوصفية',
|
||||
newAction: 'بيانات وصفية جديدة',
|
||||
manageAction: 'إدارة',
|
||||
},
|
||||
datasetMetadata: {
|
||||
description: 'يمكنك إدارة جميع البيانات الوصفية في هذه المعرفة هنا. سيتم مزامنة التعديلات مع كل مستند.',
|
||||
addMetaData: 'إضافة بيانات وصفية',
|
||||
values: '{{num}} قيم',
|
||||
disabled: 'معطل',
|
||||
rename: 'إعادة تسمية',
|
||||
name: 'الاسم',
|
||||
namePlaceholder: 'اسم البيانات الوصفية',
|
||||
builtIn: 'مدمج',
|
||||
builtInDescription: 'يتم استخراج البيانات الوصفية المدمجة وإنشاؤها تلقائيًا. يجب تمكينه قبل الاستخدام ولا يمكن تعديله.',
|
||||
deleteTitle: 'تأكيد الحذف',
|
||||
deleteContent: 'هل أنت متأكد أنك تريد حذف البيانات الوصفية "{{name}}"',
|
||||
},
|
||||
documentMetadata: {
|
||||
metadataToolTip: 'تعمل البيانات الوصفية كمرشح حاسم يعزز دقة وملاءمة استرجاع المعلومات. يمكنك تعديل وإضافة بيانات وصفية لهذا المستند هنا.',
|
||||
startLabeling: 'بدء التصنيف',
|
||||
documentInformation: 'معلومات المستند',
|
||||
technicalParameters: 'المعلمات الفنية',
|
||||
},
|
||||
},
|
||||
serviceApi: {
|
||||
title: 'واجهة برمجة تطبيقات الخدمة',
|
||||
enabled: 'في الخدمة',
|
||||
disabled: 'معطل',
|
||||
card: {
|
||||
title: 'واجهة برمجة تطبيقات خدمة الخلفية',
|
||||
endpoint: 'نقطة نهاية واجهة برمجة تطبيقات الخدمة',
|
||||
apiKey: 'مفتاح API',
|
||||
apiReference: 'مرجع API',
|
||||
},
|
||||
},
|
||||
cornerLabel: {
|
||||
unavailable: 'غير متاح',
|
||||
pipeline: 'خط أنابيب',
|
||||
},
|
||||
multimodal: 'متعدد الوسائط',
|
||||
imageUploader: {
|
||||
button: 'اسحب وأفلت الملف أو المجلد، أو',
|
||||
browse: 'تصفح',
|
||||
tip: '{{supportTypes}} (الحد الأقصى {{batchCount}}، {{size}} ميغابايت لكل منها)',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
const translation = {
|
||||
toVerified: 'احصل على التحقق التعليمي',
|
||||
toVerifiedTip: {
|
||||
front: 'أنت الآن مؤهل للحصول على حالة التحقق التعليمي. يرجى إدخال معلومات التعليم الخاصة بك أدناه لإكمال العملية والحصول على',
|
||||
coupon: 'كوبون حصري 100٪',
|
||||
end: 'لخطة Dify الاحترافية.',
|
||||
},
|
||||
currentSigned: 'تم تسجيل الدخول حاليًا باسم',
|
||||
form: {
|
||||
schoolName: {
|
||||
title: 'اسم مدرستك',
|
||||
placeholder: 'أدخل الاسم الرسمي الكامل لمدرستك',
|
||||
},
|
||||
schoolRole: {
|
||||
title: 'دورك في المدرسة',
|
||||
option: {
|
||||
student: 'طالب',
|
||||
teacher: 'معلم',
|
||||
administrator: 'مسؤول المدرسة',
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
title: 'الشروط والاتفاقيات',
|
||||
desc: {
|
||||
front: 'المعلومات الخاصة بك واستخدام حالة التحقق التعليمي تخضع لـ',
|
||||
and: 'و',
|
||||
end: '. من خلال الإرسال:',
|
||||
termsOfService: 'شروط الخدمة',
|
||||
privacyPolicy: 'سياسة الخصوصية',
|
||||
},
|
||||
option: {
|
||||
age: 'أؤكد أن عمري 18 عامًا على الأقل',
|
||||
inSchool: 'أؤكد أنني مسجل أو موظف في المؤسسة المقدمة. قد تطلب Dify إثبات التسجيل/التوظيف. إذا قدمت معلومات خاطئة حول أهليتي، فأوافق على دفع أي رسوم تم التنازل عنها مبدئيًا بناءً على حالة التعليم الخاصة بي.',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: 'إرسال',
|
||||
submitError: 'فشل إرسال النموذج. يرجى المحاولة مرة أخرى لاحقًا.',
|
||||
learn: 'تعرف على كيفية التحقق من التعليم',
|
||||
successTitle: 'لقد حصلت على التحقق التعليمي من Dify',
|
||||
successContent: 'لقد أصدرنا كوبون خصم 100٪ لخطة Dify Professional لحسابك. الكوبون ساري لمدة عام واحد، يرجى استخدامه خلال فترة الصلاحية.',
|
||||
rejectTitle: 'تم رفض التحقق التعليمي الخاص بك في Dify',
|
||||
rejectContent: 'لسوء الحظ، أنت غير مؤهل للحصول على حالة التحقق التعليمي وبالتالي لا يمكنك الحصول على كوبون حصري 100٪ لخطة Dify Professional إذا كنت تستخدم عنوان البريد الإلكتروني هذا.',
|
||||
emailLabel: 'بريدك الإلكتروني الحالي',
|
||||
notice: {
|
||||
dateFormat: 'MM/DD/YYYY',
|
||||
expired: {
|
||||
title: 'انتهت حالة التعليم الخاصة بك',
|
||||
summary: {
|
||||
line1: 'لا يزال بإمكانك الوصول إلى Dify واستخدامه. ',
|
||||
line2: 'ومع ذلك، لم تعد مؤهلاً للحصول على كوبونات خصم التعليم الجديدة.',
|
||||
},
|
||||
},
|
||||
isAboutToExpire: {
|
||||
title: 'ستنتهي حالة التعليم الخاصة بك في {{date}}',
|
||||
summary: 'لا تقلق - لن يؤثر هذا على اشتراكك الحالي، لكنك لن تحصل على خضم التعليم عند تجديده ما لم تتحقق من حالتك مرة أخرى.',
|
||||
},
|
||||
stillInEducation: {
|
||||
title: 'هل ما زلت في التعليم؟',
|
||||
expired: 'تحقق مرة أخرى الآن للحصول على كوبون جديد للعام الدراسي القادم. سنضيفه إلى حسابك ويمكنك استخدامه للترقية التالية.',
|
||||
isAboutToExpire: 'تحقق مرة أخرى الآن للحصول على كوبون جديد للعام الدراسي القادم. سيتم حفظه في حسابك وجاهز للاستخدام في تجديدك التالي.',
|
||||
},
|
||||
alreadyGraduated: {
|
||||
title: 'تخرجت بالفعل؟',
|
||||
expired: 'لا تتردد في الترقية في أي وقت للحصول على الوصول الكامل إلى الميزات المدفوعة.',
|
||||
isAboutToExpire: 'سيظل اشتراكك الحالي نشطًا. عندما ينتهي، سيتم نقلك إلى خطة Sandbox، أو يمكنك الترقية في أي وقت لاستعادة الوصول الكامل إلى الميزات المدفوعة.',
|
||||
},
|
||||
action: {
|
||||
dismiss: 'تجاهل',
|
||||
upgrade: 'ترقية',
|
||||
reVerify: 'إعادة التحقق',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
const translation = {
|
||||
title: 'استكشاف',
|
||||
sidebar: {
|
||||
discovery: 'اكتشاف',
|
||||
chat: 'دردشة',
|
||||
workspace: 'مساحة العمل',
|
||||
action: {
|
||||
pin: 'تثبيت',
|
||||
unpin: 'إلغاء التثبيت',
|
||||
rename: 'إعادة تسمية',
|
||||
delete: 'حذف',
|
||||
},
|
||||
delete: {
|
||||
title: 'حذف التطبيق',
|
||||
content: 'هل أنت متأكد أنك تريد حذف هذا التطبيق؟',
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'استكشاف التطبيقات',
|
||||
description: 'استخدم تطبيقات القوالب هذه فورًا أو خصص تطبيقاتك الخاصة بناءً على القوالب.',
|
||||
allCategories: 'موصى به',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'إضافة إلى مساحة العمل',
|
||||
customize: 'تخصيص',
|
||||
},
|
||||
appCustomize: {
|
||||
title: 'إنشاء تطبيق من {{name}}',
|
||||
subTitle: 'أيقونة التطبيق واسمه',
|
||||
nameRequired: 'اسم التطبيق مطلوب',
|
||||
},
|
||||
category: {
|
||||
Agent: 'وكيل',
|
||||
Assistant: 'مساعد',
|
||||
Writing: 'كتابة',
|
||||
Translate: 'ترجمة',
|
||||
Programming: 'برمجة',
|
||||
HR: 'الموارد البشرية',
|
||||
Workflow: 'سير العمل',
|
||||
Entertainment: 'ترفيه',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const translation = {
|
||||
sidebar: {
|
||||
expandSidebar: 'توسيع الشريط الجانبي',
|
||||
collapseSidebar: 'طي الشريط الجانبي',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
const translation = {
|
||||
pageTitle: 'تسجيل الدخول إلى Dify',
|
||||
pageTitleForE: 'مرحبًا، لنبدأ!',
|
||||
welcome: '👋 مرحبًا! يرجى تسجيل الدخول للبدء.',
|
||||
email: 'عنوان البريد الإلكتروني',
|
||||
emailPlaceholder: 'بريدك الإلكتروني',
|
||||
password: 'كلمة المرور',
|
||||
passwordPlaceholder: 'كلمة المرور الخاصة بك',
|
||||
name: 'اسم المستخدم',
|
||||
namePlaceholder: 'اسم المستخدم الخاص بك',
|
||||
forget: 'نسيت كلمة المرور؟',
|
||||
signBtn: 'تسجيل الدخول',
|
||||
continueWithCode: 'المتابعة مع الرمز',
|
||||
sendVerificationCode: 'إرسال رمز التحقق',
|
||||
usePassword: 'استخدام كلمة المرور',
|
||||
useVerificationCode: 'استخدام رمز التحقق',
|
||||
or: 'أو',
|
||||
installBtn: 'إعداد',
|
||||
setAdminAccount: 'إعداد حساب مسؤول',
|
||||
setAdminAccountDesc: 'أقصى امتيازات لحساب المسؤول، والتي يمكن استخدامها لإنشاء التطبيقات وإدارة مزودي LLM، إلخ.',
|
||||
createAndSignIn: 'إنشاء وتسجيل الدخول',
|
||||
oneMoreStep: 'خطوة واحدة أخرى',
|
||||
createSample: 'بناءً على هذه المعلومات، سنقوم بإنشاء تطبيق تجريبي لك',
|
||||
invitationCode: 'رمز الدعوة',
|
||||
invitationCodePlaceholder: 'رمز الدعوة الخاص بك',
|
||||
interfaceLanguage: 'لغة الواجهة',
|
||||
timezone: 'المنطقة الزمنية',
|
||||
go: 'الذهاب إلى Dify',
|
||||
sendUsMail: 'أرسل لنا مقدمتك عبر البريد الإلكتروني، وسنتعامل مع طلب الدعوة.',
|
||||
acceptPP: 'لقد قرأت وأوافق على سياسة الخصوصية',
|
||||
reset: 'يرجى تشغيل الأمر التالي لإعادة تعيين كلمة المرور الخاصة بك',
|
||||
withGitHub: 'المتابعة مع GitHub',
|
||||
withGoogle: 'المتابعة مع Google',
|
||||
withSSO: 'المتابعة مع SSO',
|
||||
rightTitle: 'أطلق العنان للإمكانات الكاملة لـ LLM',
|
||||
rightDesc: 'بناء تطبيقات الذكاء الاصطناعي الجذابة بصريًا والقابلة للتشغيل والقابلة للتحسين بسهولة.',
|
||||
tos: 'شروط الخدمة',
|
||||
pp: 'سياسة الخصوصية',
|
||||
tosDesc: 'بالتسجيل، فإنك توافق على',
|
||||
goToInit: 'إذا لم تقم بتهيئة الحساب، يرجى الانتقال إلى صفحة التهيئة',
|
||||
dontHave: 'ليس لديك؟',
|
||||
invalidInvitationCode: 'رمز دعوة غير صالح',
|
||||
accountAlreadyInited: 'تمت تهيئة الحساب بالفعل',
|
||||
forgotPassword: 'نسيت كلمة المرور؟',
|
||||
resetLinkSent: 'تم إرسال رابط إعادة التعيين',
|
||||
sendResetLink: 'إرسال رابط إعادة التعيين',
|
||||
backToSignIn: 'العودة لتسجيل الدخول',
|
||||
forgotPasswordDesc: 'يرجى إدخال عنوان بريدك الإلكتروني لإعادة تعيين كلمة المرور الخاصة بك. سنرسل لك بريدًا إلكترونيًا يحتوي على تعليمات حول كيفية إعادة تعيين كلمة المرور الخاصة بك.',
|
||||
checkEmailForResetLink: 'يرجى التحقق من بريدك الإلكتروني للحصول على رابط لإعادة تعيين كلمة المرور الخاصة بك. إذا لم يظهر في غضون بضع دقائق، فتأكد من التحقق من مجلد الرسائل غير المرغوب فيها.',
|
||||
passwordChanged: 'سجل الدخول الآن',
|
||||
changePassword: 'تعيين كلمة مرور',
|
||||
changePasswordTip: 'يرجى إدخال كلمة مرور جديدة لحسابك',
|
||||
changePasswordBtn: 'تعيين كلمة مرور',
|
||||
invalidToken: 'رمز غير صالح أو منتهي الصلاحية',
|
||||
confirmPassword: 'تأكيد كلمة المرور',
|
||||
confirmPasswordPlaceholder: 'تأكيد كلمة المرور الجديدة',
|
||||
passwordChangedTip: 'تم تغيير كلمة المرور الخاصة بك بنجاح',
|
||||
error: {
|
||||
emailEmpty: 'عنوان البريد الإلكتروني مطلوب',
|
||||
emailInValid: 'يرجى إدخال عنوان بريد إلكتروني صالح',
|
||||
nameEmpty: 'الاسم مطلوب',
|
||||
passwordEmpty: 'كلمة المرور مطلوبة',
|
||||
passwordLengthInValid: 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
|
||||
passwordInvalid: 'يجب أن تحتوي كلمة المرور على أحرف وأرقام، ويجب أن يكون الطول أكبر من 8',
|
||||
registrationNotAllowed: 'الحساب غير موجود. يرجى الاتصال بمسؤول النظام للتسجيل.',
|
||||
invalidEmailOrPassword: 'بريد إلكتروني أو كلمة مرور غير صالحة.',
|
||||
},
|
||||
license: {
|
||||
tip: 'قبل بدء تشغيل Dify Community Edition، اقرأ GitHub',
|
||||
link: 'ترخيص مفتوح المصدر',
|
||||
},
|
||||
join: 'انضم ',
|
||||
joinTipStart: 'يدعوك للانضمام إلى ',
|
||||
joinTipEnd: ' فريق على Dify',
|
||||
invalid: 'انتهت صلاحية الرابط',
|
||||
explore: 'استكشاف Dify',
|
||||
activatedTipStart: 'لقد انضممت إلى',
|
||||
activatedTipEnd: 'فريق',
|
||||
activated: 'سجل الدخول الآن',
|
||||
adminInitPassword: 'كلمة مرور تهيئة المسؤول',
|
||||
validate: 'تحقق',
|
||||
checkCode: {
|
||||
checkYourEmail: 'تحقق من بريدك الإلكتروني',
|
||||
tipsPrefix: 'نرسل رمز التحقق إلى ',
|
||||
validTime: 'ضع في اعتبارك أن الرمز صالح لمدة 5 دقائق',
|
||||
verificationCode: 'رمز التحقق',
|
||||
verificationCodePlaceholder: 'أدخل رمزًا مكونًا من 6 أرقام',
|
||||
verify: 'تحقق',
|
||||
didNotReceiveCode: 'لم تتلق الرمز؟ ',
|
||||
resend: 'إعادة الإرسال',
|
||||
useAnotherMethod: 'استخدام طريقة أخرى',
|
||||
emptyCode: 'الرمز مطلوب',
|
||||
invalidCode: 'رمز غير صالح',
|
||||
},
|
||||
resetPassword: 'إعادة تعيين كلمة المرور',
|
||||
resetPasswordDesc: 'اكتب البريد الإلكتروني الذي استخدمته للتسجيل في Dify وسنرسل لك بريدًا إلكترونيًا لإعادة تعيين كلمة المرور.',
|
||||
backToLogin: 'العودة لتسجيل الدخول',
|
||||
setYourAccount: 'إعداد حسابك',
|
||||
enterYourName: 'يرجى إدخال اسم المستخدم الخاص بك',
|
||||
back: 'عودة',
|
||||
noLoginMethod: 'طريقة المصادقة غير مكونة',
|
||||
noLoginMethodTip: 'يرجى الاتصال بمسؤول النظام لإضافة طريقة مصادقة.',
|
||||
licenseExpired: 'انتهت صلاحية الترخيص',
|
||||
licenseExpiredTip: 'انتهت صلاحية ترخيص Dify Enterprise لمساحة العمل الخاصة بك. يرجى الاتصال بالمسؤول لمواصلة استخدام Dify.',
|
||||
licenseLost: 'فقدان الترخيص',
|
||||
licenseLostTip: 'فشل الاتصال بخادم ترخيص Dify. يرجى الاتصال بالمسؤول لمواصلة استخدام Dify.',
|
||||
licenseInactive: 'الترخيص غير نشط',
|
||||
licenseInactiveTip: 'ترخيص Dify Enterprise لمساحة العمل الخاصة بك غير نشط. يرجى الاتصال بالمسؤول لمواصلة استخدام Dify.',
|
||||
webapp: {
|
||||
login: 'تسجيل الدخول',
|
||||
noLoginMethod: 'طريقة المصادقة غير مكونة لتطبيق الويب',
|
||||
noLoginMethodTip: 'يرجى الاتصال بمسؤول النظام لإضافة طريقة مصادقة.',
|
||||
disabled: 'مصادقة Webapp معطلة. يرجى الاتصال بمسؤول النظام لتمكينها. يمكنك محاولة استخدام التطبيق مباشرة.',
|
||||
},
|
||||
signup: {
|
||||
noAccount: 'ليس لديك حساب؟ ',
|
||||
signUp: 'اشتراك',
|
||||
createAccount: 'إنشاء حسابك',
|
||||
welcome: '👋 مرحبًا! يرجى ملء التفاصيل للبدء.',
|
||||
verifyMail: 'المتابعة مع رمز التحقق',
|
||||
haveAccount: 'لديك حساب بالفعل؟ ',
|
||||
signIn: 'تسجيل الدخول',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const translation = {
|
||||
tips: {
|
||||
loggedIn: 'يريد هذا التطبيق الوصول إلى المعلومات التالية من حساب Dify Cloud الخاص بك.',
|
||||
notLoggedIn: 'يريد هذا التطبيق الوصول إلى حساب Dify Cloud الخاص بك',
|
||||
needLogin: 'يرجى تسجيل الدخول للتفويض',
|
||||
common: 'نحن نحترم خصوصيتك وسنستخدم هذه المعلومات فقط لتحسين تجربتك مع أدوات المطورين لدينا.',
|
||||
},
|
||||
connect: 'الاتصال بـ',
|
||||
continue: 'متابعة',
|
||||
switchAccount: 'تبديل الحساب',
|
||||
login: 'تسجيل الدخول',
|
||||
scopes: {
|
||||
name: 'الاسم',
|
||||
email: 'البريد الإلكتروني',
|
||||
avatar: 'الصورة الرمزية',
|
||||
languagePreference: 'تفضيل اللغة',
|
||||
timezone: 'المنطقة الزمنية',
|
||||
},
|
||||
error: {
|
||||
invalidParams: 'معلمات غير صالحة',
|
||||
authorizeFailed: 'فشل التفويض',
|
||||
authAppInfoFetchFailed: 'فشل جلب معلومات التطبيق للتفويض',
|
||||
},
|
||||
unknownApp: 'تطبيق غير معروف',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
const translation = {
|
||||
common: {
|
||||
goToAddDocuments: 'الذهاب لإضافة مستندات',
|
||||
publishAs: 'النشر كقالب سير عمل مخصص',
|
||||
confirmPublish: 'تأكيد النشر',
|
||||
confirmPublishContent: 'بعد نشر سير عمل المعرفة بنجاح، لا يمكن تعديل هيكل التقطيع لقاعدة المعرفة هذه. هل أنت متأكد أنك تريد نشرها؟',
|
||||
publishAsPipeline: {
|
||||
name: 'اسم وأيقونة سير العمل',
|
||||
namePlaceholder: 'يرجى إدخال اسم سير عمل المعرفة هذا. (مطلوب) ',
|
||||
description: 'وصف المعرفة',
|
||||
descriptionPlaceholder: 'يرجى إدخال وصف سير عمل المعرفة هذا. (اختياري) ',
|
||||
},
|
||||
testRun: 'تشغيل اختباري',
|
||||
preparingDataSource: 'جارٍ إعداد مصدر البيانات',
|
||||
reRun: 'إعادة التشغيل',
|
||||
processing: 'جارٍ المعالجة',
|
||||
},
|
||||
inputField: {
|
||||
create: 'إنشاء حقل إدخال المستخدم',
|
||||
manage: 'إدارة',
|
||||
},
|
||||
publishToast: {
|
||||
title: 'لم يتم نشر سير العمل هذا بعد',
|
||||
desc: 'عندما لا يتم نشر سير العمل، يمكنك تعديل هيكل التقطيع في عقدة قاعدة المعرفة، وسيتم حفظ تنظيم السير العمل والتغييرات تلقائيًا كمسودة.',
|
||||
},
|
||||
result: {
|
||||
resultPreview: {
|
||||
loading: 'جاري المعالجة... ارجو الانتظار',
|
||||
error: 'حدث خطأ أثناء التنفيذ',
|
||||
viewDetails: 'عرض التفاصيل',
|
||||
footerTip: 'في وضع التشغيل الاختباري، يمكن معاينة ما يصل إلى {{count}} قطعة',
|
||||
},
|
||||
},
|
||||
ragToolSuggestions: {
|
||||
title: 'اقتراحات لـ RAG',
|
||||
noRecommendationPlugins: 'لا توجد إضافات موصى بها، ابحث عن المزيد في <CustomLink>السوق</CustomLink>',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
const translation = {
|
||||
allTags: 'كل العلامات',
|
||||
searchTags: 'البحث في العلامات',
|
||||
tags: {
|
||||
agent: 'وكيل',
|
||||
rag: 'RAG',
|
||||
search: 'بحث',
|
||||
image: 'صورة',
|
||||
videos: 'فيديوهات',
|
||||
weather: 'طقس',
|
||||
finance: 'تمويل',
|
||||
design: 'تصميم',
|
||||
travel: 'سفر',
|
||||
social: 'اجتماعي',
|
||||
news: 'أخبار',
|
||||
medical: 'طبي',
|
||||
productivity: 'إنتاجية',
|
||||
education: 'تعليم',
|
||||
business: 'أعمال',
|
||||
entertainment: 'ترفيه',
|
||||
utilities: 'أدوات مساعدة',
|
||||
other: 'أخرى',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
const translation = {
|
||||
subscription: {
|
||||
title: 'الاشتراكات',
|
||||
listNum: '{{num}} اشتراكات',
|
||||
empty: {
|
||||
title: 'لا توجد اشتراكات',
|
||||
button: 'اشتراك جديد',
|
||||
},
|
||||
createButton: {
|
||||
oauth: 'اشتراك جديد باستخدام OAuth',
|
||||
apiKey: 'اشتراك جديد باستخدام مفتاح API',
|
||||
manual: 'الصق عنوان URL لإنشاء اشتراك جديد',
|
||||
},
|
||||
createSuccess: 'تم إنشاء الاشتراك بنجاح',
|
||||
createFailed: 'فشل إنشاء الاشتراك',
|
||||
maxCount: 'الحد الأقصى {{num}} اشتراكات',
|
||||
selectPlaceholder: 'حدد اشتراكًا',
|
||||
noSubscriptionSelected: 'لم يتم تحديد أي اشتراك',
|
||||
subscriptionRemoved: 'تمت إزالة الاشتراك',
|
||||
list: {
|
||||
title: 'الاشتراكات',
|
||||
addButton: 'إضافة',
|
||||
tip: 'استلام الأحداث عبر الاشتراك',
|
||||
item: {
|
||||
enabled: 'ممكن',
|
||||
disabled: 'معطل',
|
||||
credentialType: {
|
||||
api_key: 'مفتاح API',
|
||||
oauth2: 'OAuth',
|
||||
unauthorized: 'يدوي',
|
||||
},
|
||||
actions: {
|
||||
delete: 'حذف',
|
||||
deleteConfirm: {
|
||||
title: 'حذف {{name}}؟',
|
||||
success: 'تم حذف الاشتراك {{name}} بنجاح',
|
||||
error: 'فشل حذف الاشتراك {{name}}',
|
||||
content: 'بمجرد الحذف، لا يمكن استعادة هذا الاشتراك. يرجى التأكيد.',
|
||||
contentWithApps: 'الاشتراك الحالي مشار إليه بواسطة {{count}} تطبيقات. سيؤدي حذفه إلى توقف التطبيقات المكونة عن تلقي أحداث الاشتراك.',
|
||||
confirm: 'تأكيد الحذف',
|
||||
cancel: 'إلغاء',
|
||||
confirmInputWarning: 'يرجى إدخال الاسم الصحيح للتأكيد.',
|
||||
confirmInputPlaceholder: 'أدخل "{{name}}" للتأكيد.',
|
||||
confirmInputTip: 'يرجى إدخال "{{name}}" للتأكيد.',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
active: 'نشط',
|
||||
inactive: 'غير نشط',
|
||||
},
|
||||
usedByNum: 'تستخدم من قبل {{num}} سير عمل',
|
||||
noUsed: 'لا يوجد سير عمل مستخدم',
|
||||
},
|
||||
},
|
||||
addType: {
|
||||
title: 'إضافة اشتراك',
|
||||
description: 'اختر الطريقة التي تريد بها إنشاء اشتراك المشغل الخاص بك',
|
||||
options: {
|
||||
apikey: {
|
||||
title: 'إنشاء باستخدام مفتاح API',
|
||||
description: 'إنشاء اشتراك تلقائيًا باستخدام بيانات اعتماد API',
|
||||
},
|
||||
oauth: {
|
||||
title: 'إنشاء باستخدام OAuth',
|
||||
description: 'التفويض مع منصة تابعة لجهة خارجية لإنشاء اشتراك',
|
||||
clientSettings: 'إعدادات عميل OAuth',
|
||||
clientTitle: 'عميل OAuth',
|
||||
default: 'افتراضي',
|
||||
custom: 'مخصص',
|
||||
},
|
||||
manual: {
|
||||
title: 'الإعداد اليدوي',
|
||||
description: 'الصق عنوان URL لإنشاء اشتراك جديد',
|
||||
tip: 'تكوين عنوان URL على منصة تابعة لجهة خارجية يدويًا',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
steps: {
|
||||
verify: 'تحقق',
|
||||
configuration: 'تكوين',
|
||||
},
|
||||
common: {
|
||||
cancel: 'إلغاء',
|
||||
back: 'رجوع',
|
||||
next: 'التالي',
|
||||
create: 'إنشاء',
|
||||
verify: 'تحقق',
|
||||
authorize: 'تفويض',
|
||||
creating: 'جارٍ الإنشاء...',
|
||||
verifying: 'جارٍ التحقق...',
|
||||
authorizing: 'جارٍ التفويض...',
|
||||
},
|
||||
oauthRedirectInfo: 'نظرًا لعدم العثور على أسرار عميل النظام لمزود الأداة هذا، فإن إعداده يدويًا مطلوب، بالنسبة لـ redirect_uri، يرجى الاستخدام',
|
||||
apiKey: {
|
||||
title: 'إنشاء باستخدام مفتاح API',
|
||||
verify: {
|
||||
title: 'التحقق من بيانات الاعتماد',
|
||||
description: 'يرجى تقديم بيانات اعتماد واجهة برمجة التطبيقات الخاصة بك للتحقق من الوصول',
|
||||
error: 'فشل التحقق من بيانات الاعتماد. يرجى التحقق من مفتاح API الخاص بك.',
|
||||
success: 'تم التحقق من بيانات الاعتماد بنجاح',
|
||||
},
|
||||
configuration: {
|
||||
title: 'تكوين الاشتراك',
|
||||
description: 'إعداد معلمات الاشتراك الخاصة بك',
|
||||
},
|
||||
},
|
||||
oauth: {
|
||||
title: 'إنشاء باستخدام OAuth',
|
||||
authorization: {
|
||||
title: 'تفويض OAuth',
|
||||
description: 'تفويض Dify للوصول إلى حسابك',
|
||||
redirectUrl: 'عنوان URL لإعادة التوجيه',
|
||||
redirectUrlHelp: 'استخدم عنوان URL هذا في تكوين تطبيق OAuth الخاص بك',
|
||||
authorizeButton: 'تفويض مع {{provider}}',
|
||||
waitingAuth: 'في انتظار التفويض...',
|
||||
authSuccess: 'تم التفويض بنجاح',
|
||||
authFailed: 'فشل الحصول على معلومات تفويض OAuth',
|
||||
waitingJump: 'تم التفويض، في انتظار الانتقال',
|
||||
},
|
||||
configuration: {
|
||||
title: 'تكوين الاشتراك',
|
||||
description: 'إعداد معلمات الاشتراك الخاصة بك بعد التفويض',
|
||||
success: 'تم تكوين OAuth بنجاح',
|
||||
failed: 'فشل تكوين OAuth',
|
||||
},
|
||||
remove: {
|
||||
success: 'تمت إزالة OAuth بنجاح',
|
||||
failed: 'فشل إزالة OAuth',
|
||||
},
|
||||
save: {
|
||||
success: 'تم حفظ تكوين OAuth بنجاح',
|
||||
},
|
||||
},
|
||||
manual: {
|
||||
title: 'الإعداد اليدوي',
|
||||
description: 'تكوين اشتراك web hook الخاص بك يدويًا',
|
||||
logs: {
|
||||
title: 'سجلات الطلب',
|
||||
request: 'طلب',
|
||||
loading: 'في انتظار الطلب من {{pluginName}}...',
|
||||
},
|
||||
},
|
||||
form: {
|
||||
subscriptionName: {
|
||||
label: 'اسم الاشتراك',
|
||||
placeholder: 'أدخل اسم الاشتراك',
|
||||
required: 'اسم الاشتراك مطلوب',
|
||||
},
|
||||
callbackUrl: {
|
||||
label: 'عنوان URL لرد الاتصال',
|
||||
description: 'سيتلقى عنوان URL هذا أحداث web hook',
|
||||
tooltip: 'توفير نقطة نهاية يمكن الوصول إليها بشكل عام يمكنها استلام طلبات رد الاتصال من مزود المشغل.',
|
||||
placeholder: 'جارٍ الإنشاء...',
|
||||
privateAddressWarning: 'يبدو أن عنوان URL هذا هو عنوان داخلي، مما قد يتسبب في فشل طلبات web hook. يمكنك تغيير TRIGGER_URL إلى عنوان عام.',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
createFailed: 'فشل إنشاء الاشتراك',
|
||||
verifyFailed: 'فشل التحقق من بيانات الاعتماد',
|
||||
authFailed: 'فشل التفويض',
|
||||
networkError: 'خطأ في الشبكة، يرجى المحاولة مرة أخرى',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
title: 'الأحداث المتاحة',
|
||||
description: 'الأحداث التي يمكن لمكون المشغل الإضافي هذا الاشتراك فيها',
|
||||
empty: 'لا توجد أحداث متاحة',
|
||||
event: 'حدث',
|
||||
events: 'أحداث',
|
||||
actionNum: '{{num}} {{event}} متضمن',
|
||||
item: {
|
||||
parameters: '{{count}} معلمات',
|
||||
noParameters: 'لا توجد معلمات',
|
||||
},
|
||||
output: 'إخراج',
|
||||
},
|
||||
node: {
|
||||
status: {
|
||||
warning: 'قطع الاتصال',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
const translation = {
|
||||
metadata: {
|
||||
title: 'الإضافات',
|
||||
},
|
||||
category: {
|
||||
all: 'الكل',
|
||||
models: 'نماذج',
|
||||
tools: 'أدوات',
|
||||
agents: 'استراتيجيات الوكيل',
|
||||
extensions: 'ملحقات',
|
||||
triggers: 'مشغلات',
|
||||
bundles: 'حزم',
|
||||
datasources: 'مصادر البيانات',
|
||||
},
|
||||
categorySingle: {
|
||||
model: 'نموذج',
|
||||
tool: 'أداة',
|
||||
agent: 'استراتيجية الوكيل',
|
||||
extension: 'ملحق',
|
||||
trigger: 'مشغل',
|
||||
bundle: 'حزمة',
|
||||
datasource: 'مصدر بيانات',
|
||||
},
|
||||
search: 'بحث',
|
||||
allCategories: 'جميع الفئات',
|
||||
searchCategories: 'بحث في الفئات',
|
||||
searchPlugins: 'بحث في الإضافات',
|
||||
from: 'من',
|
||||
findMoreInMarketplace: 'ابحث عن المزيد في السوق',
|
||||
searchInMarketplace: 'بحث في السوق',
|
||||
fromMarketplace: 'من السوق',
|
||||
endpointsEnabled: 'تم تمكين {{num}} مجموعة من نقاط النهاية',
|
||||
searchTools: 'بحث في الأدوات...',
|
||||
installPlugin: 'تثبيت الإضافة',
|
||||
installFrom: 'تثبيت من',
|
||||
deprecated: 'مهمل',
|
||||
list: {
|
||||
noInstalled: 'لم يتم تثبيت أي إضافات',
|
||||
notFound: 'لم يتم العثور على أي إضافات',
|
||||
source: {
|
||||
marketplace: 'تثبيت من السوق',
|
||||
github: 'تثبيت من GitHub',
|
||||
local: 'تثبيت من ملف الحزمة المحلية',
|
||||
},
|
||||
},
|
||||
source: {
|
||||
marketplace: 'السوق',
|
||||
github: 'GitHub',
|
||||
local: 'ملف الحزمة المحلية',
|
||||
},
|
||||
detailPanel: {
|
||||
switchVersion: 'تبديل الإصدار',
|
||||
categoryTip: {
|
||||
marketplace: 'مثبت من السوق',
|
||||
github: 'مثبت من Github',
|
||||
local: 'إضافة محلية',
|
||||
debugging: 'تصحيح الإضافة',
|
||||
},
|
||||
operation: {
|
||||
install: 'تثبيت',
|
||||
detail: 'التفاصيل',
|
||||
update: 'تحديث',
|
||||
info: 'معلومات الإضافة',
|
||||
checkUpdate: 'التحقق من التحديث',
|
||||
viewDetail: 'عرض التفاصيل',
|
||||
remove: 'إزالة',
|
||||
back: 'رجوع',
|
||||
},
|
||||
actionNum: '{{num}} {{action}} متضمن',
|
||||
strategyNum: '{{num}} {{strategy}} متضمن',
|
||||
endpoints: 'نقاط النهاية',
|
||||
endpointsTip: 'توفر هذه الإضافة وظائف محددة عبر نقاط النهاية، ويمكنك تكوين مجموعات نقاط نهاية متعددة لمساحة العمل الحالية.',
|
||||
endpointsDocLink: 'عرض المستند',
|
||||
endpointsEmpty: 'انقر فوق الزر "+" لإضافة نقطة نهاية',
|
||||
endpointDisableTip: 'تعطيل نقطة النهاية',
|
||||
endpointDisableContent: 'هل ترغب في تعطيل {{name}}؟ ',
|
||||
endpointDeleteTip: 'إزالة نقطة النهاية',
|
||||
endpointDeleteContent: 'هل ترغب في إزالة {{name}}؟ ',
|
||||
endpointModalTitle: 'إعداد نقطة النهاية',
|
||||
endpointModalDesc: 'بمجرد التكوين، يمكن استخدام الميزات التي توفرها الإضافة عبر نقاط نهاية API.',
|
||||
serviceOk: 'الخدمة جيدة',
|
||||
disabled: 'معطل',
|
||||
modelNum: '{{num}} نماذج متضمنة',
|
||||
toolSelector: {
|
||||
title: 'إضافة أداة',
|
||||
toolSetting: 'إعدادات الأداة',
|
||||
toolLabel: 'أداة',
|
||||
descriptionLabel: 'وصف الأداة',
|
||||
descriptionPlaceholder: 'وصف موجز لغرض الأداة، على سبيل المثال، الحصول على درجة الحرارة لموقع معين.',
|
||||
placeholder: 'حدد أداة...',
|
||||
settings: 'إعدادات المستخدم',
|
||||
params: 'تكوين الاستنتاج',
|
||||
paramsTip1: 'يتحكم في معلمات استنتاج LLM.',
|
||||
paramsTip2: 'عند إيقاف تشغيل "تلقائي"، يتم استخدام القيمة الافتراضية.',
|
||||
auto: 'تلقائي',
|
||||
empty: 'انقر فوق الزر "+" لإضافة أدوات. يمكنك إضافة أدوات متعددة.',
|
||||
uninstalledTitle: 'الأداة غير مثبتة',
|
||||
uninstalledContent: 'تم تثبيت هذه الإضافة من المخزون المحلي / GitHub. يرجى الاستخدام بعد التثبيت.',
|
||||
uninstalledLink: 'إدارة في الإضافات',
|
||||
unsupportedTitle: 'إجراء غير مدعوم',
|
||||
unsupportedContent: 'إصدار الإضافة المثبت لا يوفر هذا الإجراء.',
|
||||
unsupportedContent2: 'انقر لتبديل الإصدار.',
|
||||
unsupportedMCPTool: 'لا يدعم إصدار إضافة استراتيجية الوكيل المحدد حاليًا أدوات MCP.',
|
||||
},
|
||||
configureApp: 'تكوين التطبيق',
|
||||
configureModel: 'تكوين النموذج',
|
||||
configureTool: 'تكوين الأداة',
|
||||
deprecation: {
|
||||
fullMessage: 'تم إهمال هذه الإضافة بسبب {{deprecatedReason}}، ولن يتم تحديثها بعد الآن. يرجى استخدام <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> بدلاً من ذلك.',
|
||||
onlyReason: 'تم إهمال هذه الإضافة بسبب {{deprecatedReason}} ولن يتم تحديثها بعد الآن.',
|
||||
noReason: 'تم إهمال هذه الإضافة ولن يتم تحديثها بعد الآن.',
|
||||
reason: {
|
||||
businessAdjustments: 'تعديلات الأعمال',
|
||||
ownershipTransferred: 'نقل الملكية',
|
||||
noMaintainer: 'لا يوجد مشرف',
|
||||
},
|
||||
},
|
||||
},
|
||||
install: '{{num}} تثبيتات',
|
||||
installAction: 'تثبيت',
|
||||
debugInfo: {
|
||||
title: 'تصحيح الأخطاء',
|
||||
viewDocs: 'عرض المستندات',
|
||||
},
|
||||
privilege: {
|
||||
title: 'تفضيلات الإضافة',
|
||||
whoCanInstall: 'من يمكنه تثبيت وإدارة الإضافات؟',
|
||||
whoCanDebug: 'من يمكنه تصحيح الإضافات؟',
|
||||
everyone: 'الجميع',
|
||||
admins: 'المسؤولون',
|
||||
noone: 'لا أحد',
|
||||
},
|
||||
autoUpdate: {
|
||||
automaticUpdates: 'تحديثات تلقائية',
|
||||
updateTime: 'وقت التحديث',
|
||||
specifyPluginsToUpdate: 'تحديد الإضافات للتحديث',
|
||||
strategy: {
|
||||
disabled: {
|
||||
name: 'معطل',
|
||||
description: 'لن يتم تحديث الإضافات تلقائيًا',
|
||||
},
|
||||
fixOnly: {
|
||||
name: 'إصلاح فقط',
|
||||
description: 'التحديث التلقائي لإصدارات التصحيح فقط (على سبيل المثال، 1.0.1 → 1.0.2). لن تؤدي تغييرات الإصدار الثانوي إلى تشغيل التحديثات.',
|
||||
selectedDescription: 'التحديث التلقائي لإصدارات التصحيح فقط',
|
||||
},
|
||||
latest: {
|
||||
name: 'الأحدث',
|
||||
description: 'التحديث دائمًا إلى أحدث إصدار',
|
||||
selectedDescription: 'التحديث دائمًا إلى أحدث إصدار',
|
||||
},
|
||||
},
|
||||
updateTimeTitle: 'وقت التحديث',
|
||||
upgradeMode: {
|
||||
all: 'تحديث الكل',
|
||||
exclude: 'استبعاد المحدد',
|
||||
partial: 'المحدد فقط',
|
||||
},
|
||||
upgradeModePlaceholder: {
|
||||
exclude: 'لن يتم تحديث الإضافات المحددة تلقائيًا',
|
||||
partial: 'سيتم تحديث الإضافات المحددة فقط تلقائيًا. لم يتم تحديد أي إضافات حاليًا، لذلك لن يتم تحديث أي إضافات تلقائيًا.',
|
||||
},
|
||||
excludeUpdate: 'لن يتم تحديث الإضافات {{num}} التالية تلقائيًا',
|
||||
partialUPdate: 'سيتم تحديث الإضافات {{num}} التالية فقط تلقائيًا',
|
||||
operation: {
|
||||
clearAll: 'مسح الكل',
|
||||
select: 'تحديد الإضافات',
|
||||
},
|
||||
nextUpdateTime: 'التحديث التلقائي التالي: {{time}}',
|
||||
pluginDowngradeWarning: {
|
||||
title: 'خفض إصدار الإضافة',
|
||||
description: 'التحديث التلقائي ممكن حاليًا لهذه الإضافة. قد يؤدي خفض الإصدار إلى استبدال تغييراتك أثناء التحديث التلقائي التالي.',
|
||||
downgrade: 'خفض على أي حال',
|
||||
exclude: 'استبعاد من التحديث التلقائي',
|
||||
},
|
||||
noPluginPlaceholder: {
|
||||
noFound: 'لم يتم العثور على أي إضافات',
|
||||
noInstalled: 'لم يتم تثبيت أي إضافات',
|
||||
},
|
||||
updateSettings: 'إعدادات التحديث',
|
||||
changeTimezone: 'لتغيير المنطقة الزمنية، انتقل إلى <setTimezone>الإعدادات</setTimezone>',
|
||||
},
|
||||
pluginInfoModal: {
|
||||
title: 'معلومات الإضافة',
|
||||
repository: 'المستودع',
|
||||
release: 'الإصدار',
|
||||
packageName: 'الحزمة',
|
||||
},
|
||||
action: {
|
||||
checkForUpdates: 'التحقق من وجود تحديثات',
|
||||
pluginInfo: 'معلومات الإضافة',
|
||||
delete: 'إزالة الإضافة',
|
||||
deleteContentLeft: 'هل ترغب في إزالة ',
|
||||
deleteContentRight: ' الإضافة؟',
|
||||
usedInApps: 'يتم استخدام هذه الإضافة في {{num}} تطبيقات.',
|
||||
},
|
||||
installModal: {
|
||||
installPlugin: 'تثبيت الإضافة',
|
||||
installComplete: 'اكتمل التثبيت',
|
||||
installedSuccessfully: 'تم التثبيت بنجاح',
|
||||
installedSuccessfullyDesc: 'تم تثبيت الإضافة بنجاح.',
|
||||
uploadFailed: 'فشل التحميل',
|
||||
installFailed: 'فشل التثبيت',
|
||||
installFailedDesc: 'فشل تثبيت الإضافة.',
|
||||
install: 'تثبيت',
|
||||
installing: 'جارٍ التثبيت...',
|
||||
uploadingPackage: 'جارٍ تحميل {{packageName}}...',
|
||||
readyToInstall: 'على وشك تثبيت الإضافة التالية',
|
||||
readyToInstallPackage: 'على وشك تثبيت الإضافة التالية',
|
||||
readyToInstallPackages: 'على وشك تثبيت الإضافات {{num}} التالية',
|
||||
fromTrustSource: 'يرجى التأكد من تثبيت الإضافات فقط من <trustSource>مصدر موثوق</trustSource>.',
|
||||
dropPluginToInstall: 'أفلت حزمة الإضافة هنا للتثبيت',
|
||||
labels: {
|
||||
repository: 'المستودع',
|
||||
version: 'الإصدار',
|
||||
package: 'الحزمة',
|
||||
},
|
||||
close: 'إغلاق',
|
||||
cancel: 'إلغاء',
|
||||
back: 'رجوع',
|
||||
next: 'التالي',
|
||||
pluginLoadError: 'خطأ في تحميل الإضافة',
|
||||
pluginLoadErrorDesc: 'لن يتم تثبيت هذه الإضافة',
|
||||
installWarning: 'لا يسمح بتثبيت هذه الإضافة.',
|
||||
},
|
||||
installFromGitHub: {
|
||||
installPlugin: 'تثبيت الإضافة من GitHub',
|
||||
updatePlugin: 'تحديث الإضافة من GitHub',
|
||||
installedSuccessfully: 'تم التثبيت بنجاح',
|
||||
installFailed: 'فشل التثبيت',
|
||||
uploadFailed: 'فشل التحميل',
|
||||
gitHubRepo: 'مستودع GitHub',
|
||||
selectVersion: 'حدد الإصدار',
|
||||
selectVersionPlaceholder: 'يرجى تحديد إصدار',
|
||||
installNote: 'يرجى التأكد من تثبيت الإضافات فقط من مصدر موثوق.',
|
||||
selectPackage: 'حدد الحزمة',
|
||||
selectPackagePlaceholder: 'يرجى تحديد حزمة',
|
||||
},
|
||||
upgrade: {
|
||||
title: 'تثبيت الإضافة',
|
||||
successfulTitle: 'تم التثبيت بنجاح',
|
||||
description: 'على وشك تثبيت الإضافة التالية',
|
||||
usedInApps: 'تستخدم في {{num}} تطبيقات',
|
||||
upgrade: 'تثبيت',
|
||||
upgrading: 'جارٍ التثبيت...',
|
||||
close: 'إغلاق',
|
||||
},
|
||||
error: {
|
||||
inValidGitHubUrl: 'عنوان URL لـ GitHub غير صالح. يرجى إدخال عنوان URL صالح بالتنسيق: https://github.com/owner/repo',
|
||||
fetchReleasesError: 'غير قادر على استرجاع الإصدارات. يرجى المحاولة مرة أخرى لاحقًا.',
|
||||
noReleasesFound: 'لم يتم العثور على إصدارات. يرجى التحقق من مستودع GitHub أو عنوان URL المدخل.',
|
||||
},
|
||||
marketplace: {
|
||||
empower: 'تمكين تطوير الذكاء الاصطناعي الخاص بك',
|
||||
discover: 'اكتشف',
|
||||
and: 'و',
|
||||
difyMarketplace: 'سوق Dify',
|
||||
moreFrom: 'المزيد من السوق',
|
||||
noPluginFound: 'لم يتم العثور على إضافة',
|
||||
pluginsResult: '{{num}} نتائج',
|
||||
sortBy: 'فرز حسب',
|
||||
sortOption: {
|
||||
mostPopular: 'الأكثر شيوعًا',
|
||||
recentlyUpdated: 'تم التحديث مؤخرًا',
|
||||
newlyReleased: 'صدر حديثًا',
|
||||
firstReleased: 'صدر لأول مرة',
|
||||
},
|
||||
viewMore: 'عرض المزيد',
|
||||
verifiedTip: 'تم التحقق بواسطة Dify',
|
||||
partnerTip: 'تم التحقق بواسطة شريك Dify',
|
||||
},
|
||||
task: {
|
||||
installing: 'تثبيت {{installingLength}} إضافات، 0 تم.',
|
||||
installingWithSuccess: 'تثبيت {{installingLength}} إضافات، {{successLength}} نجاح.',
|
||||
installingWithError: 'تثبيت {{installingLength}} إضافات، {{successLength}} نجاح، {{errorLength}} فشل',
|
||||
installError: '{{errorLength}} إضافات فشل تثبيتها، انقر للعرض',
|
||||
installedError: '{{errorLength}} إضافات فشل تثبيتها',
|
||||
clearAll: 'مسح الكل',
|
||||
installSuccess: 'تم تثبيت {{successLength}} من الإضافات بنجاح',
|
||||
installed: 'مثبت',
|
||||
runningPlugins: 'تثبيت الإضافات',
|
||||
successPlugins: 'تم تثبيت الإضافات بنجاح',
|
||||
errorPlugins: 'فشل في تثبيت الإضافات',
|
||||
},
|
||||
requestAPlugin: 'طلب إضافة',
|
||||
publishPlugins: 'نشر الإضافات',
|
||||
difyVersionNotCompatible: 'إصدار Dify الحالي غير متوافق مع هذه الإضافة، يرجى الترقية إلى الحد الأدنى للإصدار المطلوب: {{minimalDifyVersion}}',
|
||||
auth: {
|
||||
default: 'افتراضي',
|
||||
custom: 'مخصص',
|
||||
setDefault: 'تعيين كافتراضي',
|
||||
useOAuth: 'استخدام OAuth',
|
||||
useOAuthAuth: 'استخدام تفويض OAuth',
|
||||
addOAuth: 'إضافة OAuth',
|
||||
setupOAuth: 'إعداد عميل OAuth',
|
||||
useApi: 'استخدام مفتاح API',
|
||||
addApi: 'إضافة مفتاح API',
|
||||
useApiAuth: 'تكوين تفويض مفتاح API',
|
||||
useApiAuthDesc: 'بعد تكوين بيانات الاعتماد، يمكن لجميع الأعضاء داخل مساحة العمل استخدام هذه الأداة عند تنظيم التطبيقات.',
|
||||
oauthClientSettings: 'إعدادات عميل OAuth',
|
||||
saveOnly: 'حفظ فقط',
|
||||
saveAndAuth: 'حفظ وتفويض',
|
||||
authorization: 'تفويض',
|
||||
authorizations: 'تفويضات',
|
||||
authorizationName: 'اسم التفويض',
|
||||
workspaceDefault: 'افتراضي مساحة العمل',
|
||||
authRemoved: 'تمت إزالة التفويض',
|
||||
clientInfo: 'نظرًا لعدم العثور على أسرار عميل النظام لمزود الأداة هذا، فإن إعداده يدويًا مطلوب، بالنسبة لـ redirect_uri، يرجى الاستخدام',
|
||||
oauthClient: 'عميل OAuth',
|
||||
credentialUnavailable: 'بيانات الاعتماد غير متوفرة حاليًا. يرجى الاتصال بالمسؤول.',
|
||||
credentialUnavailableInButton: 'بيانات الاعتماد غير متوفرة',
|
||||
customCredentialUnavailable: 'بيانات الاعتماد المخصصة غير متوفرة حاليًا',
|
||||
unavailable: 'غير متاح',
|
||||
connectedWorkspace: 'مساحة العمل المتصلة',
|
||||
emptyAuth: 'يرجى تكوين المصادقة',
|
||||
},
|
||||
readmeInfo: {
|
||||
title: 'الملف التمهيدي',
|
||||
needHelpCheckReadme: 'تحتاج للمساعدة؟ تحقق من الملف التمهيدي.',
|
||||
noReadmeAvailable: 'لا يوجد ملف تمهيدي متاح',
|
||||
failedToFetch: 'فشل جلب الملف التمهيدي',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
const translation = {
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
const translation = {
|
||||
input: 'إدخال',
|
||||
result: 'نتيجة',
|
||||
detail: 'تفاصيل',
|
||||
tracing: 'تتبع',
|
||||
resultPanel: {
|
||||
status: 'الحالة',
|
||||
time: 'الوقت المستغرق',
|
||||
tokens: 'إجمالي الرموز',
|
||||
},
|
||||
meta: {
|
||||
title: 'البيانات الوصفية',
|
||||
status: 'الحالة',
|
||||
version: 'الإصدار',
|
||||
executor: 'المنفذ',
|
||||
startTime: 'وقت البدء',
|
||||
time: 'الوقت المستغرق',
|
||||
tokens: 'إجمالي الرموز',
|
||||
steps: 'خطوات التشغيل',
|
||||
},
|
||||
resultEmpty: {
|
||||
title: 'هذا التشغيل يخرج فقط تنسيق JSON،',
|
||||
tipLeft: 'يرجى الذهاب إلى ',
|
||||
link: 'لوحة التفاصيل',
|
||||
tipRight: ' لعرضه.',
|
||||
},
|
||||
actionLogs: 'سجلات العمل',
|
||||
circularInvocationTip: 'يوجد استدعاء دائري للأدوات/العقد في سير العمل الحالي.',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
const translation = {
|
||||
common: {
|
||||
welcome: '',
|
||||
appUnavailable: 'التطبيق غير متوفر',
|
||||
appUnknownError: 'التطبيق غير متوفر',
|
||||
},
|
||||
chat: {
|
||||
newChat: 'بدء دردشة جديدة',
|
||||
newChatTip: 'موجود بالفعل في دردشة جديدة',
|
||||
chatSettingsTitle: 'إعداد الدردشة الجديدة',
|
||||
chatFormTip: 'لا يمكن تعديل إعدادات الدردشة بعد بدء الدردشة.',
|
||||
pinnedTitle: 'مثبت',
|
||||
unpinnedTitle: 'الأخيرة',
|
||||
newChatDefaultName: 'محادثة جديدة',
|
||||
resetChat: 'إعادة تعيين المحادثة',
|
||||
viewChatSettings: 'عرض إعدادات الدردشة',
|
||||
poweredBy: 'مشغل بواسطة',
|
||||
prompt: 'مطالبة',
|
||||
privatePromptConfigTitle: 'إعدادات المحادثة',
|
||||
publicPromptConfigTitle: 'المطالبة الأولية',
|
||||
configStatusDes: 'قبل البدء، يمكنك تعديل إعدادات المحادثة',
|
||||
configDisabled:
|
||||
'تم استخدام إعدادات الجلسة السابقة لهذه الجلسة.',
|
||||
startChat: 'بدء الدردشة',
|
||||
privacyPolicyLeft:
|
||||
'يرجى قراءة ',
|
||||
privacyPolicyMiddle:
|
||||
'سياسة الخصوصية',
|
||||
privacyPolicyRight:
|
||||
' المقدمة من مطور التطبيق.',
|
||||
deleteConversation: {
|
||||
title: 'حذف المحادثة',
|
||||
content: 'هل أنت متأكد أنك تريد حذف هذه المحادثة؟',
|
||||
},
|
||||
tryToSolve: 'حاول الحل',
|
||||
temporarySystemIssue: 'عذرًا، مشكلة مؤقتة في النظام.',
|
||||
expand: 'توسيع',
|
||||
collapse: 'طي',
|
||||
},
|
||||
generation: {
|
||||
tabs: {
|
||||
create: 'تشغيل مرة واحدة',
|
||||
batch: 'تشغيل دفعة',
|
||||
saved: 'محفوظ',
|
||||
},
|
||||
savedNoData: {
|
||||
title: 'لم تقم بحفظ نتيجة بعد!',
|
||||
description: 'ابدأ في إنشاء المحتوى، وابحث عن نتائجك المحفوظة هنا.',
|
||||
startCreateContent: 'ابدأ في إنشاء المحتوى',
|
||||
},
|
||||
title: 'إكمال الذكاء الاصطناعي',
|
||||
queryTitle: 'محتوى الاستعلام',
|
||||
completionResult: 'نتيجة الإكمال',
|
||||
queryPlaceholder: 'اكتب محتوى الاستعلام الخاص بك...',
|
||||
run: 'تنفيذ',
|
||||
execution: 'تشغيل',
|
||||
executions: '{{num}} عمليات تشغيل',
|
||||
copy: 'نسخ',
|
||||
resultTitle: 'إكمال الذكاء الاصطناعي',
|
||||
noData: 'سيعطيك الذكاء الاصطناعي ما تريد هنا.',
|
||||
csvUploadTitle: 'اسحب وأفلت ملف CSV هنا، أو ',
|
||||
browse: 'تصفح',
|
||||
csvStructureTitle: 'يجب أن يتوافق ملف CSV مع الهيكل التالي:',
|
||||
downloadTemplate: 'تنزيل النموذج هنا',
|
||||
field: 'حقل',
|
||||
stopRun: 'إيقاف التشغيل',
|
||||
batchFailed: {
|
||||
info: '{{num}} عمليات تنفيذ فاشلة',
|
||||
retry: 'إعادة المحاولة',
|
||||
outputPlaceholder: 'لا يوجد محتوى إخراج',
|
||||
},
|
||||
errorMsg: {
|
||||
empty: 'يرجى إدخال محتوى في الملف الذي تم تحميله.',
|
||||
fileStructNotMatch: 'ملف CSV الذي تم تحميله لا يطابق الهيكل.',
|
||||
emptyLine: 'الصف {{rowIndex}} فارغ',
|
||||
invalidLine: 'الصف {{rowIndex}}: قيمة {{varName}} لا يمكن أن تكون فارغة',
|
||||
moreThanMaxLengthLine: 'الصف {{rowIndex}}: قيمة {{varName}} لا يمكن أن تكون أكثر من {{maxLength}} حرفًا',
|
||||
atLeastOne: 'يرجى إدخال صف واحد على الأقل في الملف الذي تم تحميله.',
|
||||
},
|
||||
},
|
||||
login: {
|
||||
backToHome: 'العودة إلى الصفحة الرئيسية',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
const translation = {
|
||||
daysInWeek: {
|
||||
Sun: 'الأحد',
|
||||
Mon: 'الاثنين',
|
||||
Tue: 'الثلاثاء',
|
||||
Wed: 'الأربعاء',
|
||||
Thu: 'الخميس',
|
||||
Fri: 'الجمعة',
|
||||
Sat: 'السبت',
|
||||
},
|
||||
months: {
|
||||
January: 'يناير',
|
||||
February: 'فبراير',
|
||||
March: 'مارس',
|
||||
April: 'أبريل',
|
||||
May: 'مايو',
|
||||
June: 'يونيو',
|
||||
July: 'يوليو',
|
||||
August: 'أغسطس',
|
||||
September: 'سبتمبر',
|
||||
October: 'أكتوبر',
|
||||
November: 'نوفمبر',
|
||||
December: 'ديسمبر',
|
||||
},
|
||||
operation: {
|
||||
now: 'الآن',
|
||||
ok: 'موافق',
|
||||
cancel: 'إلغاء',
|
||||
pickDate: 'اختر التاريخ',
|
||||
},
|
||||
title: {
|
||||
pickTime: 'اختر الوقت',
|
||||
},
|
||||
defaultPlaceholder: 'اختر وقتًا...',
|
||||
// Date format configurations
|
||||
dateFormats: {
|
||||
display: 'MMMM D, YYYY',
|
||||
displayWithTime: 'MMMM D, YYYY hh:mm A',
|
||||
input: 'YYYY-MM-DD',
|
||||
output: 'YYYY-MM-DD',
|
||||
outputWithTime: 'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
const translation = {
|
||||
title: 'أدوات',
|
||||
createCustomTool: 'إنشاء أداة مخصصة',
|
||||
customToolTip: 'تعرف على المزيد حول أدوات Dify المخصصة',
|
||||
type: {
|
||||
builtIn: 'أدوات',
|
||||
custom: 'مخصص',
|
||||
workflow: 'سير عمل',
|
||||
},
|
||||
contribute: {
|
||||
line1: 'أنا مهتم بـ ',
|
||||
line2: 'المساهمة بأدوات في Dify.',
|
||||
viewGuide: 'عرض الدليل',
|
||||
},
|
||||
author: 'بواسطة',
|
||||
auth: {
|
||||
authorized: 'مفوض',
|
||||
setup: 'إعداد التفويض للاستخدام',
|
||||
setupModalTitle: 'إعداد التفويض',
|
||||
setupModalTitleDescription: 'بعد تكوين بيانات الاعتماد، يمكن لجميع الأعضاء داخل مساحة العمل استخدام هذه الأداة عند تنظيم التطبيقات.',
|
||||
},
|
||||
includeToolNum: '{{num}} {{action}} متضمن',
|
||||
addToolModal: {
|
||||
type: 'نوع',
|
||||
category: 'فئة',
|
||||
added: 'أضيف',
|
||||
custom: {
|
||||
title: 'لا توجد أداة مخصصة متاحة',
|
||||
tip: 'إنشاء أداة مخصصة',
|
||||
},
|
||||
workflow: {
|
||||
title: 'لا يوجد أداة سير عمل متاحة',
|
||||
tip: 'نشر سير العمل كأدوات في الاستوديو',
|
||||
},
|
||||
mcp: {
|
||||
title: 'لا توجد أداة MCP متاحة',
|
||||
tip: 'إضافة خادم MCP',
|
||||
},
|
||||
agent: {
|
||||
title: 'لا توجد استراتيجية وكيل متاحة',
|
||||
},
|
||||
},
|
||||
createTool: {
|
||||
title: 'إنشاء أداة مخصصة',
|
||||
editAction: 'تكوين',
|
||||
editTitle: 'تعديل أداة مخصصة',
|
||||
name: 'الاسم',
|
||||
toolNamePlaceHolder: 'أدخل اسم الأداة',
|
||||
nameForToolCall: 'اسم استدعاء الأداة',
|
||||
nameForToolCallPlaceHolder: 'يستخدم للتعرف على الآلة، مثل getCurrentWeather, list_pets',
|
||||
nameForToolCallTip: 'يدعم فقط الأرقام والحروف والشرطات السفلية.',
|
||||
description: 'الوصف',
|
||||
descriptionPlaceholder: 'وصف موجز لغرض الأداة، على سبيل المثال، الحصول على درجة الحرارة لموقع معين.',
|
||||
schema: 'المخطط',
|
||||
schemaPlaceHolder: 'أدخل مخطط OpenAPI الخاص بك هنا',
|
||||
viewSchemaSpec: 'عرض مواصفات OpenAPI-Swagger',
|
||||
importFromUrl: 'استيراد من عنوان URL',
|
||||
importFromUrlPlaceHolder: 'https://...',
|
||||
urlError: 'يرجى إدخال عنوان URL صالح',
|
||||
examples: 'أمثلة',
|
||||
exampleOptions: {
|
||||
json: 'Weather(JSON)',
|
||||
yaml: 'Pet Store(YAML)',
|
||||
blankTemplate: 'قالب فارغ',
|
||||
},
|
||||
availableTools: {
|
||||
title: 'الأدوات المتاحة',
|
||||
name: 'الاسم',
|
||||
description: 'الوصف',
|
||||
method: 'الطريقة',
|
||||
path: 'المسار',
|
||||
action: 'الإجراءات',
|
||||
test: 'اختبار',
|
||||
},
|
||||
authMethod: {
|
||||
title: 'طريقة التفويض',
|
||||
type: 'نوع التفويض',
|
||||
keyTooltip: 'مفتاح رأس Http، يمكنك تركه بـ "Authorization" إذا لم يكن لديك فكرة عما هو عليه أو تعيينه إلى قيمة مخصصة',
|
||||
queryParam: 'معلمة الاستعلام',
|
||||
queryParamTooltip: 'اسم معلمة استعلام مفتاح API للتمرير، على سبيل المثال "key" في "https://example.com/test?key=API_KEY".',
|
||||
types: {
|
||||
none: 'لا شيء',
|
||||
api_key_header: 'رأس',
|
||||
api_key_query: 'معلمة استعلام',
|
||||
apiKeyPlaceholder: 'اسم رأس HTTP لمفتاح API',
|
||||
apiValuePlaceholder: 'أدخل مفتاح API',
|
||||
queryParamPlaceholder: 'اسم معلمة الاستعلام لمفتاح API',
|
||||
},
|
||||
key: 'مفتاح',
|
||||
value: 'قيمة',
|
||||
},
|
||||
authHeaderPrefix: {
|
||||
title: 'نوع المصادقة',
|
||||
types: {
|
||||
basic: 'أساسي',
|
||||
bearer: 'Bearer',
|
||||
custom: 'مخصص',
|
||||
},
|
||||
},
|
||||
privacyPolicy: 'سياسة الخصوصية',
|
||||
privacyPolicyPlaceholder: 'يرجى إدخال سياسة الخصوصية',
|
||||
toolInput: {
|
||||
title: 'إدخال الأداة',
|
||||
name: 'الاسم',
|
||||
required: 'مطلوب',
|
||||
method: 'الطريقة',
|
||||
methodSetting: 'إعداد',
|
||||
methodSettingTip: 'يملأ المستخدم تكوين الأداة',
|
||||
methodParameter: 'معلمة',
|
||||
methodParameterTip: 'يملأ LLM أثناء الاستنتاج',
|
||||
label: 'العلامات',
|
||||
labelPlaceholder: 'اختر العلامات (اختياري)',
|
||||
description: 'الوصف',
|
||||
descriptionPlaceholder: 'وصف معنى المعلمة',
|
||||
},
|
||||
toolOutput: {
|
||||
title: 'إخراج الأداة',
|
||||
name: 'الاسم',
|
||||
reserved: 'محجوز',
|
||||
reservedParameterDuplicateTip: 'text و json و files هي متغيرات محجوزة. لا يمكن أن تظهر المتغيرات بهذه الأسماء في مخطط الإخراج.',
|
||||
description: 'الوصف',
|
||||
},
|
||||
customDisclaimer: 'إخلاء مسؤولية مخصص',
|
||||
customDisclaimerPlaceholder: 'يرجى إدخال إخلاء مسؤولية مخصص',
|
||||
confirmTitle: 'تأكيد الحفظ؟',
|
||||
confirmTip: 'ستتأثر التطبيقات التي تستخدم هذه الأداة',
|
||||
deleteToolConfirmTitle: 'حذف هذه الأداة؟',
|
||||
deleteToolConfirmContent: 'حذف الأداة لا رجعة فيه. لن يتمكن المستخدمون بعد الآن من الوصول إلى أداتك.',
|
||||
},
|
||||
test: {
|
||||
title: 'اختبار',
|
||||
parametersValue: 'المعلمات والقيمة',
|
||||
parameters: 'المعلمات',
|
||||
value: 'القيمة',
|
||||
testResult: 'نتائج الاختبار',
|
||||
testResultPlaceholder: 'ستظهر نتيجة الاختبار هنا',
|
||||
},
|
||||
thought: {
|
||||
using: 'يستخدم',
|
||||
used: 'مستخدم',
|
||||
requestTitle: 'طلب',
|
||||
responseTitle: 'استجابة',
|
||||
},
|
||||
setBuiltInTools: {
|
||||
info: 'معلومات',
|
||||
setting: 'إعداد',
|
||||
toolDescription: 'وصف الأداة',
|
||||
parameters: 'معلمات',
|
||||
string: 'سلسلة',
|
||||
number: 'رقم',
|
||||
file: 'ملف',
|
||||
required: 'مطلوب',
|
||||
infoAndSetting: 'المعلومات والإعدادات',
|
||||
},
|
||||
noCustomTool: {
|
||||
title: 'لا توجد أدوات مخصصة!',
|
||||
content: 'أضف وأدر أدواتك المخصصة هنا لبناء تطبيقات الذكاء الاصطناعي.',
|
||||
createTool: 'إنشاء أداة',
|
||||
},
|
||||
noSearchRes: {
|
||||
title: 'عذرًا، لا توجد نتائج!',
|
||||
content: 'لم نتمكن من العثور على أي أدوات تطابق بحثك.',
|
||||
reset: 'إعادة تعيين البحث',
|
||||
},
|
||||
builtInPromptTitle: 'موجه',
|
||||
toolRemoved: 'تمت إزالة الأداة',
|
||||
notAuthorized: 'غير مفوض',
|
||||
howToGet: 'كيفية الحصول على',
|
||||
openInStudio: 'فتح في الاستوديو',
|
||||
toolNameUsageTip: 'اسم استدعاء الأداة لمنطق الوكيل والتحفيز',
|
||||
copyToolName: 'نسخ الاسم',
|
||||
noTools: 'لم يتم العثور على أدوات',
|
||||
mcp: {
|
||||
create: {
|
||||
cardTitle: 'إضافة خادم MCP (HTTP)',
|
||||
cardLink: 'تعرف على المزيد حول تكامل خادم MCP',
|
||||
},
|
||||
noConfigured: 'غير مكون',
|
||||
updateTime: 'محدث',
|
||||
toolsCount: '{{count}} أدوات',
|
||||
noTools: 'لا توجد أدوات متاحة',
|
||||
modal: {
|
||||
title: 'إضافة خادم MCP (HTTP)',
|
||||
editTitle: 'تعديل خادم MCP (HTTP)',
|
||||
name: 'الاسم والأيقونة',
|
||||
namePlaceholder: 'قم بتسمية خادم MCP الخاص بك',
|
||||
serverUrl: 'عنوان URL للخادم',
|
||||
serverUrlPlaceholder: 'عنوان URL لنقطة نهاية الخادم',
|
||||
serverUrlWarning: 'قد يؤدي تحديث عنوان الخادم إلى تعطيل التطبيقات التي تعتمد على هذا الخادم',
|
||||
serverIdentifier: 'معرف الخادم',
|
||||
serverIdentifierTip: 'معرف فريد لخادم MCP داخل مساحة العمل. أحرف صغيرة وأرقام وشرطات سفلية وواصلات فقط. ما يصل إلى 24 حرفًا.',
|
||||
serverIdentifierPlaceholder: 'معرف فريد، على سبيل المثال، my-mcp-server',
|
||||
serverIdentifierWarning: 'لن يتم التعرف على الخادم بواسطة التطبيقات الموجودة بعد تغيير المعرف',
|
||||
headers: 'رؤوس',
|
||||
headersTip: 'رؤوس HTTP إضافية للإرسال مع طلبات خادم MCP',
|
||||
headerKey: 'اسم الرأس',
|
||||
headerValue: 'قيمة الرأس',
|
||||
headerKeyPlaceholder: 'على سبيل المثال، Authorization',
|
||||
headerValuePlaceholder: 'على سبيل المثال، Bearer token123',
|
||||
addHeader: 'إضافة رأس',
|
||||
noHeaders: 'لم يتم تكوين رؤوس مخصصة',
|
||||
maskedHeadersTip: 'يتم إخفاء قيم الرأس للأمان. ستقوم التغييرات بتحديث القيم الفعلية.',
|
||||
cancel: 'إلغاء',
|
||||
save: 'حفظ',
|
||||
confirm: 'إضافة وتفويض',
|
||||
timeout: 'مهلة',
|
||||
sseReadTimeout: 'مهلة قراءة SSE',
|
||||
timeoutPlaceholder: '30',
|
||||
authentication: 'المصادقة',
|
||||
useDynamicClientRegistration: 'استخدام تسجيل العميل الديناميكي',
|
||||
redirectUrlWarning: 'يرجى تكوين عنوان URL لإعادة توجيه OAuth الخاص بك إلى:',
|
||||
clientID: 'معرف العميل',
|
||||
clientSecret: 'سر العميل',
|
||||
clientSecretPlaceholder: 'سر العميل',
|
||||
configurations: 'التكوينات',
|
||||
},
|
||||
delete: 'إزالة خادم MCP',
|
||||
deleteConfirmTitle: 'هل ترغب في إزالة {{mcp}}؟',
|
||||
operation: {
|
||||
edit: 'تعديل',
|
||||
remove: 'إزالة',
|
||||
},
|
||||
authorize: 'تفويض',
|
||||
authorizing: 'جارٍ التفويض...',
|
||||
authorizingRequired: 'التفويض مطلوب',
|
||||
authorizeTip: 'بعد التفويض، سيتم عرض الأدوات هنا.',
|
||||
update: 'تحديث',
|
||||
updating: 'جارٍ التحديث',
|
||||
gettingTools: 'جارٍ الحصول على الأدوات...',
|
||||
updateTools: 'جارٍ تحديث الأدوات...',
|
||||
toolsEmpty: 'لم يتم تحميل الأدوات',
|
||||
getTools: 'احصل على الأدوات',
|
||||
toolUpdateConfirmTitle: 'تحديث قائمة الأدوات',
|
||||
toolUpdateConfirmContent: 'قد يؤثر تحديث قائمة الأدوات على التطبيقات الموجودة. هل ترغب في المتابعة؟',
|
||||
toolsNum: '{{count}} أدوات متضمنة',
|
||||
onlyTool: 'أداة واحدة متضمنة',
|
||||
identifier: 'معرف الخادم (انقر للنسخ)',
|
||||
server: {
|
||||
title: 'خادم MCP',
|
||||
url: 'عنوان URL للخادم',
|
||||
reGen: 'هل تريد إعادة إنشاء عنوان URL للخادم؟',
|
||||
addDescription: 'إضافة وصف',
|
||||
edit: 'تعديل الوصف',
|
||||
modal: {
|
||||
addTitle: 'إضافة وصف لتمكين خادم MCP',
|
||||
editTitle: 'تعديل الوصف',
|
||||
description: 'الوصف',
|
||||
descriptionPlaceholder: 'اشرح ما تفعله هذه الأداة وكيف يجب استخدامها بواسطة LLM',
|
||||
parameters: 'المعلمات',
|
||||
parametersTip: 'أضف أوصافًا لكل معلمة لمساعدة LLM على فهم الغرض منها والقيود المفروضة عليها.',
|
||||
parametersPlaceholder: 'الغرض من المعلمة والقيود',
|
||||
confirm: 'تمكين خادم MCP',
|
||||
},
|
||||
publishTip: 'التطبيق غير منشور. يرجى نشر التطبيق أولاً.',
|
||||
},
|
||||
toolItem: {
|
||||
noDescription: 'لا يوجد وصف',
|
||||
parameters: 'المعلمات',
|
||||
},
|
||||
},
|
||||
allTools: 'جميع الأدوات',
|
||||
}
|
||||
|
||||
export default translation
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'Quelle',
|
||||
text: 'Text',
|
||||
time: 'Zeit',
|
||||
queryContent: 'Inhaltsabfrage',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ const translation = {
|
|||
variable: 'Verwende die Variable',
|
||||
},
|
||||
inputVars: 'Eingabevariablen',
|
||||
pluginNotInstalled: 'Plugin ist nicht installiert',
|
||||
},
|
||||
start: {
|
||||
required: 'erforderlich',
|
||||
|
|
|
|||
|
|
@ -468,6 +468,7 @@ const translation = {
|
|||
variable: 'Use variable',
|
||||
},
|
||||
inputVars: 'Input Variables',
|
||||
pluginNotInstalled: 'Plugin is not installed',
|
||||
},
|
||||
start: {
|
||||
required: 'required',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'Fuente',
|
||||
text: 'Texto',
|
||||
time: 'Tiempo',
|
||||
queryContent: 'Contenido de la consulta',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ const translation = {
|
|||
variable: 'Usa la variable',
|
||||
},
|
||||
inputVars: 'Variables de entrada',
|
||||
pluginNotInstalled: 'El complemento no está instalado',
|
||||
},
|
||||
start: {
|
||||
required: 'requerido',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'منبع',
|
||||
text: 'متن',
|
||||
time: 'زمان',
|
||||
queryContent: 'محتوای پرسوجو',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ const translation = {
|
|||
variable: 'از متغیر استفاده کن',
|
||||
},
|
||||
inputVars: 'متغیرهای ورودی',
|
||||
pluginNotInstalled: 'افزونه نصب نشده است',
|
||||
},
|
||||
start: {
|
||||
required: 'الزامی',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'Source',
|
||||
text: 'Texte',
|
||||
time: 'Temps',
|
||||
queryContent: 'Contenu de la requête',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ const translation = {
|
|||
variable: 'Utilisez une variable',
|
||||
},
|
||||
inputVars: 'Variables d’entrée',
|
||||
pluginNotInstalled: 'Le plugin n\'est pas installé',
|
||||
},
|
||||
start: {
|
||||
required: 'requis',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'स्रोत',
|
||||
text: 'पाठ',
|
||||
time: 'समय',
|
||||
queryContent: 'सवाल की सामग्री',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -449,6 +449,7 @@ const translation = {
|
|||
variable: 'चर का प्रयोग करें',
|
||||
},
|
||||
inputVars: 'इनपुट चर',
|
||||
pluginNotInstalled: 'प्लगइन इंस्टॉल नहीं है',
|
||||
},
|
||||
start: {
|
||||
required: 'आवश्यक',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
const translation = {
|
||||
table: {
|
||||
header: {
|
||||
text: 'Teks',
|
||||
source: 'Sumber',
|
||||
time: 'Waktu',
|
||||
queryContent: 'Konten Query',
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ const translation = {
|
|||
insertVarTip: 'Sisipkan Variabel',
|
||||
outputVars: 'Variabel Keluaran',
|
||||
inputVars: 'Variabel Masukan',
|
||||
pluginNotInstalled: 'Plugin tidak terpasang',
|
||||
},
|
||||
start: {
|
||||
outputVars: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'Fonte',
|
||||
text: 'Testo',
|
||||
time: 'Ora',
|
||||
queryContent: 'Contenuto della query',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@ const translation = {
|
|||
variable: 'Usa la variabile',
|
||||
},
|
||||
inputVars: 'Variabili di input',
|
||||
pluginNotInstalled: 'Il plugin non è installato',
|
||||
},
|
||||
start: {
|
||||
required: 'richiesto',
|
||||
|
|
|
|||
|
|
@ -464,6 +464,7 @@ const translation = {
|
|||
variable: '変数を使用する',
|
||||
},
|
||||
inputVars: '入力変数',
|
||||
pluginNotInstalled: 'プラグインがインストールされていません',
|
||||
},
|
||||
start: {
|
||||
required: '必須',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: '소스',
|
||||
text: '텍스트',
|
||||
time: '시간',
|
||||
queryContent: '질의 내용',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ const translation = {
|
|||
variable: '변수를 사용하세요',
|
||||
},
|
||||
inputVars: '입력 변수',
|
||||
pluginNotInstalled: '플러그인이 설치되지 않았습니다',
|
||||
},
|
||||
start: {
|
||||
required: '필수',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const translation = {
|
|||
table: {
|
||||
header: {
|
||||
source: 'Źródło',
|
||||
text: 'Tekst',
|
||||
time: 'Czas',
|
||||
queryContent: 'Treść zapytania',
|
||||
},
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue