ميزه: إضافة ميزات جديدة لتحرير PDF، OCR، وإزالة الخلفية مع تفعيل خيارات في ملف البيئة

This commit is contained in:
Your Name
2026-03-08 22:51:50 +02:00
parent d7f6228d7f
commit 0a0c069a58
16 changed files with 242 additions and 62 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

Binary file not shown.

View File

@@ -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):

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 />

View 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;
}
}

View File

@@ -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'),

View File

@@ -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>
</> </>

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {