ميزه: إضافة ميزات جديدة لتحرير PDF، OCR، وإزالة الخلفية مع تفعيل خيارات في ملف البيئة
This commit is contained in:
@@ -33,3 +33,8 @@ VITE_ADSENSE_SLOT_HOME_TOP=1234567890
|
|||||||
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891
|
||||||
VITE_ADSENSE_SLOT_TOP_BANNER=1234567892
|
VITE_ADSENSE_SLOT_TOP_BANNER=1234567892
|
||||||
VITE_ADSENSE_SLOT_BOTTOM_BANNER=1234567893
|
VITE_ADSENSE_SLOT_BOTTOM_BANNER=1234567893
|
||||||
|
|
||||||
|
# Feature Flags (set to "false" to disable a specific tool)
|
||||||
|
FEATURE_EDITOR=true
|
||||||
|
FEATURE_OCR=true
|
||||||
|
FEATURE_REMOVEBG=true
|
||||||
|
|||||||
@@ -17,10 +17,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
tesseract-ocr-eng \
|
tesseract-ocr-eng \
|
||||||
tesseract-ocr-ara \
|
tesseract-ocr-ara \
|
||||||
tesseract-ocr-fra \
|
tesseract-ocr-fra \
|
||||||
|
poppler-utils \
|
||||||
|
default-jre-headless \
|
||||||
curl \
|
curl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Ensure Java is on PATH for tabula-py (extract-tables, pdf-to-excel)
|
||||||
|
ENV JAVA_HOME=/usr/lib/jvm/default-java
|
||||||
|
ENV PATH="${JAVA_HOME}/bin:${PATH}"
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ ALLOWED_OCR_TYPES = ALLOWED_IMAGE_TYPES + ["pdf"]
|
|||||||
|
|
||||||
|
|
||||||
def _check_feature_flag():
|
def _check_feature_flag():
|
||||||
"""Return an error response if FEATURE_EDITOR is disabled."""
|
"""Return an error response if FEATURE_OCR is disabled."""
|
||||||
if not current_app.config.get("FEATURE_EDITOR", False):
|
if not current_app.config.get("FEATURE_OCR", True):
|
||||||
return jsonify({"error": "This feature is not enabled."}), 403
|
return jsonify({"error": "This feature is not enabled."}), 403
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def remove_bg_route():
|
|||||||
- 'file': Image file (PNG, JPG, JPEG, WebP)
|
- 'file': Image file (PNG, JPG, JPEG, WebP)
|
||||||
Returns: JSON with task_id for polling
|
Returns: JSON with task_id for polling
|
||||||
"""
|
"""
|
||||||
if not current_app.config.get("FEATURE_EDITOR", False):
|
if not current_app.config.get("FEATURE_REMOVEBG", True):
|
||||||
return jsonify({"error": "This feature is not enabled."}), 403
|
return jsonify({"error": "This feature is not enabled."}), 403
|
||||||
|
|
||||||
if "file" not in request.files:
|
if "file" not in request.files:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import requests
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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")
|
||||||
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3-8b-instruct")
|
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3-8b-instruct")
|
||||||
OPENROUTER_BASE_URL = os.getenv(
|
OPENROUTER_BASE_URL = os.getenv(
|
||||||
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions"
|
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions"
|
||||||
@@ -219,38 +219,50 @@ def extract_tables(input_path: str) -> dict:
|
|||||||
{"tables": [...], "tables_found": int}
|
{"tables": [...], "tables_found": int}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import tabula
|
import tabula # type: ignore[import-untyped]
|
||||||
|
from PyPDF2 import PdfReader
|
||||||
|
|
||||||
tables = tabula.read_pdf(
|
# Get total page count
|
||||||
input_path, pages="all", multiple_tables=True, silent=True
|
reader = PdfReader(input_path)
|
||||||
)
|
total_pages = len(reader.pages)
|
||||||
|
|
||||||
if not tables:
|
result_tables = []
|
||||||
|
table_index = 0
|
||||||
|
|
||||||
|
for page_num in range(1, total_pages + 1):
|
||||||
|
page_tables = tabula.read_pdf(
|
||||||
|
input_path, pages=str(page_num), multiple_tables=True, silent=True
|
||||||
|
)
|
||||||
|
if not page_tables:
|
||||||
|
continue
|
||||||
|
for df in page_tables:
|
||||||
|
if df.empty:
|
||||||
|
continue
|
||||||
|
headers = [str(c) for c in df.columns]
|
||||||
|
rows = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
cells = []
|
||||||
|
for col in df.columns:
|
||||||
|
val = row[col]
|
||||||
|
if isinstance(val, float) and str(val) == "nan":
|
||||||
|
cells.append("")
|
||||||
|
else:
|
||||||
|
cells.append(str(val))
|
||||||
|
rows.append(cells)
|
||||||
|
|
||||||
|
result_tables.append({
|
||||||
|
"page": page_num,
|
||||||
|
"table_index": table_index,
|
||||||
|
"headers": headers,
|
||||||
|
"rows": rows,
|
||||||
|
})
|
||||||
|
table_index += 1
|
||||||
|
|
||||||
|
if not result_tables:
|
||||||
raise PdfAiError(
|
raise PdfAiError(
|
||||||
"No tables found in the PDF. This tool works best with PDFs containing tabular data."
|
"No tables found in the PDF. This tool works best with PDFs containing tabular data."
|
||||||
)
|
)
|
||||||
|
|
||||||
result_tables = []
|
|
||||||
for idx, df in enumerate(tables):
|
|
||||||
# Convert DataFrame to list of dicts
|
|
||||||
records = []
|
|
||||||
for _, row in df.iterrows():
|
|
||||||
record = {}
|
|
||||||
for col in df.columns:
|
|
||||||
val = row[col]
|
|
||||||
if isinstance(val, float) and str(val) == "nan":
|
|
||||||
record[str(col)] = ""
|
|
||||||
else:
|
|
||||||
record[str(col)] = str(val)
|
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
result_tables.append({
|
|
||||||
"index": idx + 1,
|
|
||||||
"columns": [str(c) for c in df.columns],
|
|
||||||
"rows": len(records),
|
|
||||||
"data": records,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Extracted {len(result_tables)} tables from PDF")
|
logger.info(f"Extracted {len(result_tables)} tables from PDF")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
BIN
backend/celerybeat-schedule
Normal file
BIN
backend/celerybeat-schedule
Normal file
Binary file not shown.
@@ -80,7 +80,7 @@ class BaseConfig:
|
|||||||
RATELIMIT_DEFAULT = "100/hour"
|
RATELIMIT_DEFAULT = "100/hour"
|
||||||
|
|
||||||
# OpenRouter AI
|
# OpenRouter AI
|
||||||
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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")
|
||||||
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3-8b-instruct")
|
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3-8b-instruct")
|
||||||
OPENROUTER_BASE_URL = os.getenv(
|
OPENROUTER_BASE_URL = os.getenv(
|
||||||
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions"
|
"OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions"
|
||||||
@@ -95,8 +95,10 @@ class BaseConfig:
|
|||||||
SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
|
SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
|
||||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
||||||
|
|
||||||
# Feature flags
|
# Feature flags (default: enabled — set to "false" to disable a feature)
|
||||||
FEATURE_EDITOR = os.getenv("FEATURE_EDITOR", "false").lower() == "true"
|
FEATURE_EDITOR = os.getenv("FEATURE_EDITOR", "true").lower() == "true"
|
||||||
|
FEATURE_OCR = os.getenv("FEATURE_OCR", "true").lower() == "true"
|
||||||
|
FEATURE_REMOVEBG = os.getenv("FEATURE_REMOVEBG", "true").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentConfig(BaseConfig):
|
class DevelopmentConfig(BaseConfig):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from tests.conftest import make_png_bytes, make_pdf_bytes
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
class TestOcrFeatureFlag:
|
class TestOcrFeatureFlag:
|
||||||
def test_ocr_image_disabled_by_default(self, client):
|
def test_ocr_image_disabled_by_default(self, client):
|
||||||
"""OCR image should return 403 when FEATURE_EDITOR is off."""
|
"""OCR image should return 403 when FEATURE_OCR is off."""
|
||||||
data = {"file": (io.BytesIO(make_png_bytes()), "test.png")}
|
data = {"file": (io.BytesIO(make_png_bytes()), "test.png")}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/ocr/image",
|
"/api/ocr/image",
|
||||||
@@ -24,7 +24,7 @@ class TestOcrFeatureFlag:
|
|||||||
assert "not enabled" in response.get_json()["error"]
|
assert "not enabled" in response.get_json()["error"]
|
||||||
|
|
||||||
def test_ocr_pdf_disabled_by_default(self, client):
|
def test_ocr_pdf_disabled_by_default(self, client):
|
||||||
"""OCR PDF should return 403 when FEATURE_EDITOR is off."""
|
"""OCR PDF should return 403 when FEATURE_OCR is off."""
|
||||||
data = {"file": (io.BytesIO(make_pdf_bytes()), "scan.pdf")}
|
data = {"file": (io.BytesIO(make_pdf_bytes()), "scan.pdf")}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/ocr/pdf",
|
"/api/ocr/pdf",
|
||||||
@@ -50,14 +50,14 @@ class TestOcrFeatureFlag:
|
|||||||
class TestOcrValidation:
|
class TestOcrValidation:
|
||||||
def test_ocr_image_no_file(self, client, app):
|
def test_ocr_image_no_file(self, client, app):
|
||||||
"""Should return 400 when no file provided."""
|
"""Should return 400 when no file provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_OCR"] = True
|
||||||
response = client.post("/api/ocr/image")
|
response = client.post("/api/ocr/image")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "No file" in response.get_json()["error"]
|
assert "No file" in response.get_json()["error"]
|
||||||
|
|
||||||
def test_ocr_pdf_no_file(self, client, app):
|
def test_ocr_pdf_no_file(self, client, app):
|
||||||
"""Should return 400 when no file provided."""
|
"""Should return 400 when no file provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_OCR"] = True
|
||||||
response = client.post("/api/ocr/pdf")
|
response = client.post("/api/ocr/pdf")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "No file" in response.get_json()["error"]
|
assert "No file" in response.get_json()["error"]
|
||||||
@@ -69,7 +69,7 @@ class TestOcrValidation:
|
|||||||
class TestOcrSuccess:
|
class TestOcrSuccess:
|
||||||
def test_ocr_image_success(self, client, app, monkeypatch):
|
def test_ocr_image_success(self, client, app, monkeypatch):
|
||||||
"""Should return 202 with task_id when valid image provided."""
|
"""Should return 202 with task_id when valid image provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_OCR"] = True
|
||||||
mock_task = MagicMock()
|
mock_task = MagicMock()
|
||||||
mock_task.id = "ocr-img-task-1"
|
mock_task.id = "ocr-img-task-1"
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ class TestOcrSuccess:
|
|||||||
|
|
||||||
def test_ocr_pdf_success(self, client, app, monkeypatch):
|
def test_ocr_pdf_success(self, client, app, monkeypatch):
|
||||||
"""Should return 202 with task_id when valid PDF provided."""
|
"""Should return 202 with task_id when valid PDF provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_OCR"] = True
|
||||||
mock_task = MagicMock()
|
mock_task = MagicMock()
|
||||||
mock_task.id = "ocr-pdf-task-1"
|
mock_task.id = "ocr-pdf-task-1"
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class TestOcrSuccess:
|
|||||||
|
|
||||||
def test_ocr_image_invalid_lang_falls_back(self, client, app, monkeypatch):
|
def test_ocr_image_invalid_lang_falls_back(self, client, app, monkeypatch):
|
||||||
"""Invalid lang should fall back to 'eng' without error."""
|
"""Invalid lang should fall back to 'eng' without error."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_OCR"] = True
|
||||||
mock_task = MagicMock()
|
mock_task = MagicMock()
|
||||||
mock_task.id = "ocr-lang-task"
|
mock_task.id = "ocr-lang-task"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from tests.conftest import make_png_bytes, make_pdf_bytes
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
class TestRemoveBgFeatureFlag:
|
class TestRemoveBgFeatureFlag:
|
||||||
def test_removebg_disabled_by_default(self, client):
|
def test_removebg_disabled_by_default(self, client):
|
||||||
"""Should return 403 when FEATURE_EDITOR is off."""
|
"""Should return 403 when FEATURE_REMOVEBG is off."""
|
||||||
data = {"file": (io.BytesIO(make_png_bytes()), "photo.png")}
|
data = {"file": (io.BytesIO(make_png_bytes()), "photo.png")}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/remove-bg",
|
"/api/remove-bg",
|
||||||
@@ -29,7 +29,7 @@ class TestRemoveBgFeatureFlag:
|
|||||||
class TestRemoveBgValidation:
|
class TestRemoveBgValidation:
|
||||||
def test_removebg_no_file(self, client, app):
|
def test_removebg_no_file(self, client, app):
|
||||||
"""Should return 400 when no file provided."""
|
"""Should return 400 when no file provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_REMOVEBG"] = True
|
||||||
response = client.post("/api/remove-bg")
|
response = client.post("/api/remove-bg")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "No file" in response.get_json()["error"]
|
assert "No file" in response.get_json()["error"]
|
||||||
@@ -41,7 +41,7 @@ class TestRemoveBgValidation:
|
|||||||
class TestRemoveBgSuccess:
|
class TestRemoveBgSuccess:
|
||||||
def test_removebg_success(self, client, app, monkeypatch):
|
def test_removebg_success(self, client, app, monkeypatch):
|
||||||
"""Should return 202 with task_id when valid image provided."""
|
"""Should return 202 with task_id when valid image provided."""
|
||||||
app.config["FEATURE_EDITOR"] = True
|
app.config["FEATURE_REMOVEBG"] = True
|
||||||
mock_task = MagicMock()
|
mock_task = MagicMock()
|
||||||
mock_task.id = "rembg-task-1"
|
mock_task.id = "rembg-task-1"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { lazy, Suspense, useEffect } from 'react';
|
|||||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import Header from '@/components/layout/Header';
|
import Header from '@/components/layout/Header';
|
||||||
import Footer from '@/components/layout/Footer';
|
import Footer from '@/components/layout/Footer';
|
||||||
|
import ErrorBoundary from '@/components/shared/ErrorBoundary';
|
||||||
import { useDirection } from '@/hooks/useDirection';
|
import { useDirection } from '@/hooks/useDirection';
|
||||||
import { initAnalytics, trackPageView } from '@/services/analytics';
|
import { initAnalytics, trackPageView } from '@/services/analytics';
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
@@ -77,6 +78,7 @@ export default function App() {
|
|||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className="container mx-auto flex-1 px-4 py-8 sm:px-6 lg:px-8">
|
<main className="container mx-auto flex-1 px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Pages */}
|
{/* Pages */}
|
||||||
@@ -140,6 +142,7 @@ export default function App() {
|
|||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
48
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
48
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component, type ReactNode } from 'react';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallbackMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { hasError: false };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): State {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReset = () => {
|
||||||
|
this.setState({ hasError: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-lg py-16 text-center">
|
||||||
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
|
||||||
|
<AlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-2 text-xl font-semibold text-slate-800 dark:text-slate-200">
|
||||||
|
{this.props.fallbackMessage || 'Something went wrong'}
|
||||||
|
</h2>
|
||||||
|
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
An unexpected error occurred. Please try again.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="rounded-lg bg-primary-600 px-6 py-2 text-sm font-medium text-white hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ export default function QrCodeGenerator() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadUrl = result?.download_url ? `/api${result.download_url}` : null;
|
const downloadUrl = result?.download_url || null;
|
||||||
|
|
||||||
const schema = generateToolSchema({
|
const schema = generateToolSchema({
|
||||||
name: t('tools.qrCode.title'),
|
name: t('tools.qrCode.title'),
|
||||||
|
|||||||
@@ -143,6 +143,17 @@ export default function RemoveBackground() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{phase === 'done' && !result && taskError && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-xl bg-red-50 p-4 text-red-600 dark:bg-red-900/20 dark:text-red-400">
|
||||||
|
{taskError}
|
||||||
|
</div>
|
||||||
|
<button onClick={handleReset} className="btn-secondary w-full">
|
||||||
|
{t('common.tryAgain')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<AdSlot slot="bottom-banner" format="horizontal" className="mt-6" />
|
<AdSlot slot="bottom-banner" format="horizontal" className="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -23,7 +23,18 @@
|
|||||||
"email": "البريد الإلكتروني",
|
"email": "البريد الإلكتروني",
|
||||||
"password": "كلمة المرور",
|
"password": "كلمة المرور",
|
||||||
"darkMode": "الوضع الداكن",
|
"darkMode": "الوضع الداكن",
|
||||||
"lightMode": "الوضع الفاتح"
|
"lightMode": "الوضع الفاتح",
|
||||||
|
"errors": {
|
||||||
|
"fileTooLarge": "حجم الملف كبير جدًا. الحد الأقصى المسموح {{size}} ميجابايت.",
|
||||||
|
"invalidFileType": "نوع الملف غير صالح. الأنواع المقبولة: {{types}}",
|
||||||
|
"uploadFailed": "فشل رفع الملف. يرجى المحاولة مرة أخرى.",
|
||||||
|
"processingFailed": "فشلت المعالجة. يرجى المحاولة مرة أخرى.",
|
||||||
|
"quotaExceeded": "تم استنفاد حصة الاستخدام الشهرية. يرجى المحاولة الشهر القادم.",
|
||||||
|
"rateLimited": "طلبات كثيرة جدًا. يرجى الانتظار لحظة والمحاولة مجددًا.",
|
||||||
|
"serverError": "حدث خطأ في الخادم. يرجى المحاولة لاحقًا.",
|
||||||
|
"networkError": "خطأ في الشبكة. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
|
||||||
|
"noFileSelected": "لم يتم اختيار ملف. يرجى اختيار ملف للرفع."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgotPassword": {
|
"forgotPassword": {
|
||||||
@@ -167,7 +178,9 @@
|
|||||||
"selectFiles": "اختر ملفات PDF",
|
"selectFiles": "اختر ملفات PDF",
|
||||||
"addMore": "أضف ملفات أخرى",
|
"addMore": "أضف ملفات أخرى",
|
||||||
"filesSelected": "{{count}} ملفات مختارة",
|
"filesSelected": "{{count}} ملفات مختارة",
|
||||||
"dragToReorder": "اسحب الملفات لإعادة ترتيبها"
|
"dragToReorder": "اسحب الملفات لإعادة ترتيبها",
|
||||||
|
"invalidFiles": "يرجى اختيار ملفات PDF صالحة.",
|
||||||
|
"minFiles": "يرجى اختيار ملفَين على الأقل لدمجهما."
|
||||||
},
|
},
|
||||||
"splitPdf": {
|
"splitPdf": {
|
||||||
"title": "تقسيم PDF",
|
"title": "تقسيم PDF",
|
||||||
@@ -205,7 +218,13 @@
|
|||||||
"dpiLow": "72 — شاشة",
|
"dpiLow": "72 — شاشة",
|
||||||
"dpiMedium": "150 — قياسي",
|
"dpiMedium": "150 — قياسي",
|
||||||
"dpiHigh": "200 — جيد",
|
"dpiHigh": "200 — جيد",
|
||||||
"dpiUltra": "300 — جودة طباعة"
|
"dpiUltra": "300 — جودة طباعة",
|
||||||
|
"outputFormat": "صيغة الإخراج",
|
||||||
|
"quality": "الجودة",
|
||||||
|
"lowQuality": "شاشة",
|
||||||
|
"mediumQuality": "قياسي",
|
||||||
|
"highQuality": "جيد",
|
||||||
|
"bestQuality": "جودة طباعة"
|
||||||
},
|
},
|
||||||
"imagesToPdf": {
|
"imagesToPdf": {
|
||||||
"title": "صور إلى PDF",
|
"title": "صور إلى PDF",
|
||||||
@@ -213,7 +232,9 @@
|
|||||||
"shortDesc": "صور → PDF",
|
"shortDesc": "صور → PDF",
|
||||||
"selectImages": "اختر الصور",
|
"selectImages": "اختر الصور",
|
||||||
"addMore": "أضف صور أخرى",
|
"addMore": "أضف صور أخرى",
|
||||||
"imagesSelected": "{{count}} صور مختارة"
|
"imagesSelected": "{{count}} صور مختارة",
|
||||||
|
"invalidFiles": "يرجى اختيار ملفات صور صالحة (JPG أو PNG أو WebP).",
|
||||||
|
"minFiles": "يرجى اختيار صورة واحدة على الأقل."
|
||||||
},
|
},
|
||||||
"watermarkPdf": {
|
"watermarkPdf": {
|
||||||
"title": "علامة مائية PDF",
|
"title": "علامة مائية PDF",
|
||||||
@@ -393,7 +414,12 @@
|
|||||||
"pdfToExcel": {
|
"pdfToExcel": {
|
||||||
"title": "PDF إلى Excel",
|
"title": "PDF إلى Excel",
|
||||||
"description": "استخرج الجداول من ملفات PDF وحوّلها إلى جداول بيانات Excel.",
|
"description": "استخرج الجداول من ملفات PDF وحوّلها إلى جداول بيانات Excel.",
|
||||||
"shortDesc": "PDF → Excel"
|
"shortDesc": "PDF → Excel",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "لم يتم العثور على جداول في هذا الملف. يرجى استخدام ملف PDF يحتوي على بيانات جدولية.",
|
||||||
|
"processingFailed": "فشل التحويل إلى Excel. يرجى تجربة ملف PDF مختلف.",
|
||||||
|
"invalidFile": "ملف PDF غير صالح أو تالف. يرجى رفع ملف PDF صحيح."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"removeWatermark": {
|
"removeWatermark": {
|
||||||
"title": "إزالة العلامة المائية",
|
"title": "إزالة العلامة المائية",
|
||||||
@@ -460,7 +486,12 @@
|
|||||||
"shortDesc": "استخراج الجداول",
|
"shortDesc": "استخراج الجداول",
|
||||||
"tablesFound": "تم العثور على {{count}} جدول(جداول)",
|
"tablesFound": "تم العثور على {{count}} جدول(جداول)",
|
||||||
"tablePage": "الصفحة {{page}} — الجدول {{index}}",
|
"tablePage": "الصفحة {{page}} — الجدول {{index}}",
|
||||||
"noTables": "لم يتم العثور على جداول في هذا المستند."
|
"noTables": "لم يتم العثور على جداول في هذا المستند.",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "لم يتم العثور على جداول في هذا الملف. تعمل الأداة بشكل أفضل مع ملفات PDF التي تحتوي على بيانات جدولية.",
|
||||||
|
"processingFailed": "فشل استخراج الجداول. يرجى تجربة ملف PDF مختلف.",
|
||||||
|
"invalidFile": "ملف PDF غير صالح أو تالف. يرجى رفع ملف PDF صحيح."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
|||||||
@@ -23,7 +23,18 @@
|
|||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
"lightMode": "Light Mode"
|
"lightMode": "Light Mode",
|
||||||
|
"errors": {
|
||||||
|
"fileTooLarge": "File is too large. Maximum size is {{size}}MB.",
|
||||||
|
"invalidFileType": "Invalid file type. Accepted: {{types}}",
|
||||||
|
"uploadFailed": "Upload failed. Please try again.",
|
||||||
|
"processingFailed": "Processing failed. Please try again.",
|
||||||
|
"quotaExceeded": "Monthly usage limit reached. Please try again next month.",
|
||||||
|
"rateLimited": "Too many requests. Please wait a moment and try again.",
|
||||||
|
"serverError": "A server error occurred. Please try again later.",
|
||||||
|
"networkError": "Network error. Please check your connection and try again.",
|
||||||
|
"noFileSelected": "No file selected. Please choose a file to upload."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgotPassword": {
|
"forgotPassword": {
|
||||||
@@ -167,7 +178,9 @@
|
|||||||
"selectFiles": "Select PDF Files",
|
"selectFiles": "Select PDF Files",
|
||||||
"addMore": "Add More Files",
|
"addMore": "Add More Files",
|
||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
"dragToReorder": "Drag files to reorder them"
|
"dragToReorder": "Drag files to reorder them",
|
||||||
|
"invalidFiles": "Please select valid PDF files.",
|
||||||
|
"minFiles": "Please select at least 2 PDF files to merge."
|
||||||
},
|
},
|
||||||
"splitPdf": {
|
"splitPdf": {
|
||||||
"title": "Split PDF",
|
"title": "Split PDF",
|
||||||
@@ -205,7 +218,13 @@
|
|||||||
"dpiLow": "72 — Screen",
|
"dpiLow": "72 — Screen",
|
||||||
"dpiMedium": "150 — Standard",
|
"dpiMedium": "150 — Standard",
|
||||||
"dpiHigh": "200 — Good",
|
"dpiHigh": "200 — Good",
|
||||||
"dpiUltra": "300 — Print Quality"
|
"dpiUltra": "300 — Print Quality",
|
||||||
|
"outputFormat": "Output Format",
|
||||||
|
"quality": "Quality",
|
||||||
|
"lowQuality": "Screen",
|
||||||
|
"mediumQuality": "Standard",
|
||||||
|
"highQuality": "Good",
|
||||||
|
"bestQuality": "Print Quality"
|
||||||
},
|
},
|
||||||
"imagesToPdf": {
|
"imagesToPdf": {
|
||||||
"title": "Images to PDF",
|
"title": "Images to PDF",
|
||||||
@@ -213,7 +232,9 @@
|
|||||||
"shortDesc": "Images → PDF",
|
"shortDesc": "Images → PDF",
|
||||||
"selectImages": "Select Images",
|
"selectImages": "Select Images",
|
||||||
"addMore": "Add More Images",
|
"addMore": "Add More Images",
|
||||||
"imagesSelected": "{{count}} images selected"
|
"imagesSelected": "{{count}} images selected",
|
||||||
|
"invalidFiles": "Please select valid image files (JPG, PNG, WebP).",
|
||||||
|
"minFiles": "Please select at least one image."
|
||||||
},
|
},
|
||||||
"watermarkPdf": {
|
"watermarkPdf": {
|
||||||
"title": "Watermark PDF",
|
"title": "Watermark PDF",
|
||||||
@@ -393,7 +414,12 @@
|
|||||||
"pdfToExcel": {
|
"pdfToExcel": {
|
||||||
"title": "PDF to Excel",
|
"title": "PDF to Excel",
|
||||||
"description": "Extract tables from PDF files and convert them to Excel spreadsheets.",
|
"description": "Extract tables from PDF files and convert them to Excel spreadsheets.",
|
||||||
"shortDesc": "PDF → Excel"
|
"shortDesc": "PDF → Excel",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "No tables found in this PDF. Please use a PDF that contains tabular data.",
|
||||||
|
"processingFailed": "Failed to convert to Excel. Please try a different PDF.",
|
||||||
|
"invalidFile": "Invalid or corrupted PDF file. Please upload a valid PDF."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"removeWatermark": {
|
"removeWatermark": {
|
||||||
"title": "Remove Watermark",
|
"title": "Remove Watermark",
|
||||||
@@ -460,7 +486,12 @@
|
|||||||
"shortDesc": "Extract Tables",
|
"shortDesc": "Extract Tables",
|
||||||
"tablesFound": "{{count}} table(s) found",
|
"tablesFound": "{{count}} table(s) found",
|
||||||
"tablePage": "Page {{page}} — Table {{index}}",
|
"tablePage": "Page {{page}} — Table {{index}}",
|
||||||
"noTables": "No tables were found in this document."
|
"noTables": "No tables were found in this document.",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "No tables found in this PDF. This tool works best with PDFs containing tabular data.",
|
||||||
|
"processingFailed": "Failed to extract tables. Please try a different PDF.",
|
||||||
|
"invalidFile": "Invalid or corrupted PDF file. Please upload a valid PDF."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
|||||||
@@ -23,7 +23,18 @@
|
|||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"darkMode": "Mode sombre",
|
"darkMode": "Mode sombre",
|
||||||
"lightMode": "Mode clair"
|
"lightMode": "Mode clair",
|
||||||
|
"errors": {
|
||||||
|
"fileTooLarge": "Fichier trop volumineux. Taille maximale autorisée : {{size}} Mo.",
|
||||||
|
"invalidFileType": "Type de fichier non valide. Formats acceptés : {{types}}",
|
||||||
|
"uploadFailed": "Téléchargement échoué. Veuillez réessayer.",
|
||||||
|
"processingFailed": "Échec du traitement. Veuillez réessayer.",
|
||||||
|
"quotaExceeded": "Limite d'utilisation mensuelle atteinte. Veuillez réessayer le mois prochain.",
|
||||||
|
"rateLimited": "Trop de requêtes. Veuillez attendre un moment et réessayer.",
|
||||||
|
"serverError": "Une erreur serveur s'est produite. Veuillez réessayer plus tard.",
|
||||||
|
"networkError": "Erreur réseau. Veuillez vérifier votre connexion et réessayer.",
|
||||||
|
"noFileSelected": "Aucun fichier sélectionné. Veuillez choisir un fichier à télécharger."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgotPassword": {
|
"forgotPassword": {
|
||||||
@@ -167,7 +178,9 @@
|
|||||||
"selectFiles": "Sélectionner des fichiers PDF",
|
"selectFiles": "Sélectionner des fichiers PDF",
|
||||||
"addMore": "Ajouter plus de fichiers",
|
"addMore": "Ajouter plus de fichiers",
|
||||||
"filesSelected": "{{count}} fichiers sélectionnés",
|
"filesSelected": "{{count}} fichiers sélectionnés",
|
||||||
"dragToReorder": "Glissez les fichiers pour les réorganiser"
|
"dragToReorder": "Glissez les fichiers pour les réorganiser",
|
||||||
|
"invalidFiles": "Veuillez sélectionner des fichiers PDF valides.",
|
||||||
|
"minFiles": "Veuillez sélectionner au moins 2 fichiers PDF à fusionner."
|
||||||
},
|
},
|
||||||
"splitPdf": {
|
"splitPdf": {
|
||||||
"title": "Diviser PDF",
|
"title": "Diviser PDF",
|
||||||
@@ -205,7 +218,13 @@
|
|||||||
"dpiLow": "72 — Écran",
|
"dpiLow": "72 — Écran",
|
||||||
"dpiMedium": "150 — Standard",
|
"dpiMedium": "150 — Standard",
|
||||||
"dpiHigh": "200 — Bon",
|
"dpiHigh": "200 — Bon",
|
||||||
"dpiUltra": "300 — Qualité d'impression"
|
"dpiUltra": "300 — Qualité d'impression",
|
||||||
|
"outputFormat": "Format de sortie",
|
||||||
|
"quality": "Qualité",
|
||||||
|
"lowQuality": "Écran",
|
||||||
|
"mediumQuality": "Standard",
|
||||||
|
"highQuality": "Bon",
|
||||||
|
"bestQuality": "Qualité impression"
|
||||||
},
|
},
|
||||||
"imagesToPdf": {
|
"imagesToPdf": {
|
||||||
"title": "Images en PDF",
|
"title": "Images en PDF",
|
||||||
@@ -213,7 +232,9 @@
|
|||||||
"shortDesc": "Images → PDF",
|
"shortDesc": "Images → PDF",
|
||||||
"selectImages": "Sélectionner des images",
|
"selectImages": "Sélectionner des images",
|
||||||
"addMore": "Ajouter plus d'images",
|
"addMore": "Ajouter plus d'images",
|
||||||
"imagesSelected": "{{count}} images sélectionnées"
|
"imagesSelected": "{{count}} images sélectionnées",
|
||||||
|
"invalidFiles": "Veuillez sélectionner des fichiers images valides (JPG, PNG, WebP).",
|
||||||
|
"minFiles": "Veuillez sélectionner au moins une image."
|
||||||
},
|
},
|
||||||
"watermarkPdf": {
|
"watermarkPdf": {
|
||||||
"title": "Filigrane PDF",
|
"title": "Filigrane PDF",
|
||||||
@@ -393,7 +414,12 @@
|
|||||||
"pdfToExcel": {
|
"pdfToExcel": {
|
||||||
"title": "PDF vers Excel",
|
"title": "PDF vers Excel",
|
||||||
"description": "Extrayez les tableaux des fichiers PDF et convertissez-les en feuilles de calcul Excel.",
|
"description": "Extrayez les tableaux des fichiers PDF et convertissez-les en feuilles de calcul Excel.",
|
||||||
"shortDesc": "PDF → Excel"
|
"shortDesc": "PDF → Excel",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "Aucun tableau trouvé dans ce PDF. Veuillez utiliser un PDF contenant des données tabulaires.",
|
||||||
|
"processingFailed": "Échec de la conversion en Excel. Veuillez essayer un autre PDF.",
|
||||||
|
"invalidFile": "Fichier PDF invalide ou corrompu. Veuillez télécharger un PDF valide."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"removeWatermark": {
|
"removeWatermark": {
|
||||||
"title": "Supprimer le filigrane",
|
"title": "Supprimer le filigrane",
|
||||||
@@ -460,7 +486,12 @@
|
|||||||
"shortDesc": "Extraire les tableaux",
|
"shortDesc": "Extraire les tableaux",
|
||||||
"tablesFound": "{{count}} tableau(x) trouvé(s)",
|
"tablesFound": "{{count}} tableau(x) trouvé(s)",
|
||||||
"tablePage": "Page {{page}} — Tableau {{index}}",
|
"tablePage": "Page {{page}} — Tableau {{index}}",
|
||||||
"noTables": "Aucun tableau n'a été trouvé dans ce document."
|
"noTables": "Aucun tableau n'a été trouvé dans ce document.",
|
||||||
|
"errors": {
|
||||||
|
"noTables": "Aucun tableau trouvé dans ce PDF. Cet outil fonctionne mieux avec des PDF contenant des données tabulaires.",
|
||||||
|
"processingFailed": "Échec de l'extraction des tableaux. Veuillez essayer un autre PDF.",
|
||||||
|
"invalidFile": "Fichier PDF invalide ou corrompu. Veuillez télécharger un PDF valide."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
|||||||
Reference in New Issue
Block a user