feat(i18n): update translations and improve error handling messages
- Updated site tagline and footer description in multiple languages. - Enhanced error messages for various scenarios in the API service. - Added translations for new error codes related to AI features and PDF processing. - Improved user feedback in the UI components by utilizing i18n for dynamic text. - Refactored error handling in the API service to map backend error codes to user-friendly messages.
This commit is contained in:
@@ -12,7 +12,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"seo:generate": "node scripts/merge-keywords.mjs && node scripts/generate-seo-assets.mjs"
|
"seo:generate": "node scripts/merge-keywords.mjs && node scripts/generate-seo-assets.mjs",
|
||||||
|
"lint:i18n": "node scripts/check-i18n-keys.mjs",
|
||||||
|
"lint:hardcoded": "node scripts/check-hardcoded-text.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/clarity": "^1.0.2",
|
"@microsoft/clarity": "^1.0.2",
|
||||||
|
|||||||
127
frontend/scripts/check-hardcoded-text.mjs
Normal file
127
frontend/scripts/check-hardcoded-text.mjs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* check-hardcoded-text.mjs
|
||||||
|
* Scans shared/layout components for hardcoded English UI strings that should
|
||||||
|
* be replaced with t() calls.
|
||||||
|
*
|
||||||
|
* Heuristic: a JSX text node is flagged when it:
|
||||||
|
* - contains at least one space (multi-word)
|
||||||
|
* - is longer than 3 characters
|
||||||
|
* - starts with an uppercase letter or common English word
|
||||||
|
* - is NOT already wrapped in {t(...)}
|
||||||
|
* - is NOT a CSS class, URL, comment, code attribute, or aria-label value
|
||||||
|
*
|
||||||
|
* Usage: node scripts/check-hardcoded-text.mjs
|
||||||
|
* Exit code 1 if any potential hardcoded strings are found.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||||
|
import { join, relative } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
const ROOT = join(__dirname, '..');
|
||||||
|
|
||||||
|
/** Directories to scan */
|
||||||
|
const SCAN_DIRS = [
|
||||||
|
join(ROOT, 'src', 'components', 'shared'),
|
||||||
|
join(ROOT, 'src', 'components', 'layout'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Collect .tsx files
|
||||||
|
function* walkFiles(dir) {
|
||||||
|
for (const entry of readdirSync(dir)) {
|
||||||
|
const full = join(dir, entry);
|
||||||
|
if (statSync(full).isDirectory()) {
|
||||||
|
yield* walkFiles(full);
|
||||||
|
} else if (/\.tsx$/.test(entry)) {
|
||||||
|
yield full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patterns that indicate the string is NOT a hardcoded UI label:
|
||||||
|
* - Only digits/punctuation
|
||||||
|
* - Looks like a URL, path, class name, CSS value
|
||||||
|
* - Already inside {t(…)}
|
||||||
|
* - Attribute values like className, href, src, id, type, etc.
|
||||||
|
*/
|
||||||
|
const SKIP_RE = [
|
||||||
|
/^[\d\s.,/:%-]+$/, // pure numbers/punctuation
|
||||||
|
/^https?:\/\//, // URLs
|
||||||
|
/^\/[a-z]/, // paths
|
||||||
|
/^[a-z][-a-z0-9]*$/, // single lowercase word (CSS class, attr value)
|
||||||
|
/^[a-z][a-zA-Z0-9]*=[a-z]/, // key=value attrs
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldSkip(str) {
|
||||||
|
const trimmed = str.trim();
|
||||||
|
if (trimmed.length <= 3) return true;
|
||||||
|
if (!/\s/.test(trimmed)) return true; // single word
|
||||||
|
if (!/[A-Z]/.test(trimmed[0])) return true; // doesn't start with uppercase
|
||||||
|
for (const re of SKIP_RE) if (re.test(trimmed)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find JSX text content that is hardcoded English.
|
||||||
|
* Strategy: look for lines where text appears between JSX tags but is NOT
|
||||||
|
* wrapped in a {…} expression.
|
||||||
|
*
|
||||||
|
* Pattern: > Some Text Here < (with optional leading whitespace)
|
||||||
|
*/
|
||||||
|
const JSX_TEXT_RE = />\s*([A-Z][^<{}\n]{3,}?)\s*</g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Also catch string literals used directly as prop values that look like
|
||||||
|
* display text: title="Some English Text" (but not className, id, etc.)
|
||||||
|
*/
|
||||||
|
const DISPLAY_PROP_RE = /(?:title|label|placeholder|aria-label|alt)="([^"]{4,})"/g;
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
|
||||||
|
for (const dir of SCAN_DIRS) {
|
||||||
|
for (const file of walkFiles(dir)) {
|
||||||
|
const rel = relative(ROOT, file).replace(/\\/g, '/');
|
||||||
|
const content = readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const [lineIdx, line] of lines.entries()) {
|
||||||
|
// Skip comment lines
|
||||||
|
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) continue;
|
||||||
|
// Skip lines that are already pure t() calls
|
||||||
|
if (/\{t\(/.test(line)) continue;
|
||||||
|
|
||||||
|
// JSX text between tags
|
||||||
|
let m;
|
||||||
|
JSX_TEXT_RE.lastIndex = 0;
|
||||||
|
while ((m = JSX_TEXT_RE.exec(line)) !== null) {
|
||||||
|
const text = m[1].trim();
|
||||||
|
if (!shouldSkip(text)) {
|
||||||
|
findings.push({ file: rel, line: lineIdx + 1, text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display props with raw English strings
|
||||||
|
DISPLAY_PROP_RE.lastIndex = 0;
|
||||||
|
while ((m = DISPLAY_PROP_RE.exec(line)) !== null) {
|
||||||
|
const text = m[1].trim();
|
||||||
|
if (!shouldSkip(text)) {
|
||||||
|
findings.push({ file: rel, line: lineIdx + 1, text: `[prop] ${text}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findings.length === 0) {
|
||||||
|
console.log('✓ No hardcoded UI text found in shared/layout components');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠ Found ${findings.length} potential hardcoded string(s):\n`);
|
||||||
|
for (const { file, line, text } of findings) {
|
||||||
|
console.warn(` ${file}:${line} → "${text}"`);
|
||||||
|
}
|
||||||
|
// Exit 1 to allow failing CI; change to process.exit(0) to make it advisory only
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
83
frontend/scripts/check-i18n-keys.mjs
Normal file
83
frontend/scripts/check-i18n-keys.mjs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* check-i18n-keys.mjs
|
||||||
|
* Scans all .ts/.tsx files in src/ and verifies every static t('key') call
|
||||||
|
* exists as a dot-path entry in src/i18n/en.json.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/check-i18n-keys.mjs
|
||||||
|
* Exit code 1 if any missing keys are found.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||||
|
import { join, relative } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
const ROOT = join(__dirname, '..');
|
||||||
|
const SRC = join(ROOT, 'src');
|
||||||
|
const EN_JSON = join(ROOT, 'src', 'i18n', 'en.json');
|
||||||
|
|
||||||
|
// Load en.json and build a flat Set of all dot-paths
|
||||||
|
function flattenKeys(obj, prefix = '') {
|
||||||
|
const keys = new Set();
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
const path = prefix ? `${prefix}.${k}` : k;
|
||||||
|
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
||||||
|
for (const nested of flattenKeys(v, path)) keys.add(nested);
|
||||||
|
} else {
|
||||||
|
keys.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enJson = JSON.parse(readFileSync(EN_JSON, 'utf8'));
|
||||||
|
const definedKeys = flattenKeys(enJson);
|
||||||
|
|
||||||
|
// Collect all .ts/.tsx files under src/
|
||||||
|
function* walkFiles(dir) {
|
||||||
|
for (const entry of readdirSync(dir)) {
|
||||||
|
const full = join(dir, entry);
|
||||||
|
if (statSync(full).isDirectory()) {
|
||||||
|
yield* walkFiles(full);
|
||||||
|
} else if (/\.(tsx?|jsx?)$/.test(entry) && !entry.endsWith('.d.ts')) {
|
||||||
|
yield full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract static string arguments from t('...') or t("...") calls.
|
||||||
|
// Matches: t('key'), t("key"), t(`key`), useTranslation().t('key'),
|
||||||
|
// as well as i18n.t('key') patterns.
|
||||||
|
const T_CALL_RE = /\bt\(\s*(['"`])([^'"`\s]+)\1/g;
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
for (const file of walkFiles(SRC)) {
|
||||||
|
const rel = relative(ROOT, file).replace(/\\/g, '/');
|
||||||
|
const content = readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const [lineIdx, line] of lines.entries()) {
|
||||||
|
let match;
|
||||||
|
T_CALL_RE.lastIndex = 0;
|
||||||
|
while ((match = T_CALL_RE.exec(line)) !== null) {
|
||||||
|
const key = match[2];
|
||||||
|
// Skip dynamic keys (contain ${) or non-string patterns
|
||||||
|
if (key.includes('${') || key.includes('(')) continue;
|
||||||
|
if (!definedKeys.has(key)) {
|
||||||
|
missing.push({ file: rel, line: lineIdx + 1, key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length === 0) {
|
||||||
|
console.log('✓ All t() keys are present in en.json');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Found ${missing.length} missing i18n key(s):\n`);
|
||||||
|
for (const { file, line, key } of missing) {
|
||||||
|
console.error(` ${file}:${line} → "${key}"`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -2,49 +2,64 @@ import { Link } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ArrowRight, FileText, Layers3 } from 'lucide-react';
|
import { ArrowRight, FileText, Layers3 } from 'lucide-react';
|
||||||
|
|
||||||
const FOOTER_TOOLS = {
|
interface FooterTool {
|
||||||
|
slug: string;
|
||||||
|
i18nKey: string;
|
||||||
|
isLanding?: boolean;
|
||||||
|
isComparison?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOOTER_TOOLS: Record<string, FooterTool[]> = {
|
||||||
PDF: [
|
PDF: [
|
||||||
{ slug: 'pdf-to-word', label: 'PDF to Word' },
|
{ slug: 'pdf-to-word', i18nKey: 'tools.pdfToWord.title' },
|
||||||
{ slug: 'compress-pdf', label: 'Compress PDF' },
|
{ slug: 'compress-pdf', i18nKey: 'tools.compressPdf.title' },
|
||||||
{ slug: 'merge-pdf', label: 'Merge PDF' },
|
{ slug: 'merge-pdf', i18nKey: 'tools.mergePdf.title' },
|
||||||
{ slug: 'split-pdf', label: 'Split PDF' },
|
{ slug: 'split-pdf', i18nKey: 'tools.splitPdf.title' },
|
||||||
{ slug: 'pdf-to-images', label: 'PDF to Images' },
|
{ slug: 'pdf-to-images', i18nKey: 'tools.pdfToImages.title' },
|
||||||
{ slug: 'protect-pdf', label: 'Protect PDF' },
|
{ slug: 'protect-pdf', i18nKey: 'tools.protectPdf.title' },
|
||||||
{ slug: 'watermark-pdf', label: 'Watermark PDF' },
|
{ slug: 'watermark-pdf', i18nKey: 'tools.watermarkPdf.title' },
|
||||||
{ slug: 'pdf-editor', label: 'PDF Editor' },
|
{ slug: 'pdf-editor', i18nKey: 'tools.pdfEditor.title' },
|
||||||
],
|
],
|
||||||
'Image & Convert': [
|
'Image & Convert': [
|
||||||
{ slug: 'compress-image', label: 'Compress Image' },
|
{ slug: 'compress-image', i18nKey: 'tools.compressImage.title' },
|
||||||
{ slug: 'image-converter', label: 'Image Converter' },
|
{ slug: 'image-converter', i18nKey: 'tools.imageConvert.title' },
|
||||||
{ slug: 'image-resize', label: 'Image Resize' },
|
{ slug: 'image-resize', i18nKey: 'tools.imageResize.title' },
|
||||||
{ slug: 'remove-background', label: 'Remove Background' },
|
{ slug: 'remove-background', i18nKey: 'tools.removeBg.title' },
|
||||||
{ slug: 'word-to-pdf', label: 'Word to PDF' },
|
{ slug: 'word-to-pdf', i18nKey: 'tools.wordToPdf.title' },
|
||||||
{ slug: 'html-to-pdf', label: 'HTML to PDF' },
|
{ slug: 'html-to-pdf', i18nKey: 'tools.htmlToPdf.title' },
|
||||||
{ slug: 'pdf-to-excel', label: 'PDF to Excel' },
|
{ slug: 'pdf-to-excel', i18nKey: 'tools.pdfToExcel.title' },
|
||||||
],
|
],
|
||||||
'AI & Utility': [
|
'AI & Utility': [
|
||||||
{ slug: 'chat-pdf', label: 'Chat with PDF' },
|
{ slug: 'chat-pdf', i18nKey: 'tools.chatPdf.title' },
|
||||||
{ slug: 'summarize-pdf', label: 'Summarize PDF' },
|
{ slug: 'summarize-pdf', i18nKey: 'tools.summarizePdf.title' },
|
||||||
{ slug: 'translate-pdf', label: 'Translate PDF' },
|
{ slug: 'translate-pdf', i18nKey: 'tools.translatePdf.title' },
|
||||||
{ slug: 'ocr', label: 'OCR' },
|
{ slug: 'ocr', i18nKey: 'tools.ocr.title' },
|
||||||
{ slug: 'qr-code', label: 'QR Code Generator' },
|
{ slug: 'qr-code', i18nKey: 'tools.qrCode.title' },
|
||||||
{ slug: 'video-to-gif', label: 'Video to GIF' },
|
{ slug: 'video-to-gif', i18nKey: 'tools.videoToGif.title' },
|
||||||
{ slug: 'word-counter', label: 'Word Counter' },
|
{ slug: 'word-counter', i18nKey: 'tools.wordCounter.title' },
|
||||||
],
|
],
|
||||||
Guides: [
|
Guides: [
|
||||||
{ slug: 'best-pdf-tools', label: 'Best PDF Tools', isLanding: true },
|
{ slug: 'best-pdf-tools', i18nKey: 'footer.guides.bestPdfTools', isLanding: true },
|
||||||
{ slug: 'free-pdf-tools-online', label: 'Free PDF Tools Online', isLanding: true },
|
{ slug: 'free-pdf-tools-online', i18nKey: 'footer.guides.freePdfToolsOnline', isLanding: true },
|
||||||
{ slug: 'convert-files-online', label: 'Convert Files Online', isLanding: true },
|
{ slug: 'convert-files-online', i18nKey: 'footer.guides.convertFilesOnline', isLanding: true },
|
||||||
],
|
],
|
||||||
Comparisons: [
|
Comparisons: [
|
||||||
{ slug: 'compress-pdf-vs-ilovepdf', label: 'Dociva vs iLovePDF', isComparison: true },
|
{ slug: 'compress-pdf-vs-ilovepdf', i18nKey: 'footer.comparisons.compressPdfVsIlovepdf', isComparison: true },
|
||||||
{ slug: 'merge-pdf-vs-smallpdf', label: 'Dociva vs Smallpdf', isComparison: true },
|
{ slug: 'merge-pdf-vs-smallpdf', i18nKey: 'footer.comparisons.mergePdfVsSmallpdf', isComparison: true },
|
||||||
{ slug: 'pdf-to-word-vs-adobe-acrobat', label: 'Dociva vs Adobe Acrobat', isComparison: true },
|
{ slug: 'pdf-to-word-vs-adobe-acrobat', i18nKey: 'footer.comparisons.pdfToWordVsAdobeAcrobat', isComparison: true },
|
||||||
{ slug: 'compress-image-vs-tinypng', label: 'Dociva vs TinyPNG', isComparison: true },
|
{ slug: 'compress-image-vs-tinypng', i18nKey: 'footer.comparisons.compressImageVsTinypng', isComparison: true },
|
||||||
{ slug: 'ocr-vs-adobe-scan', label: 'Dociva vs Adobe Scan', isComparison: true },
|
{ slug: 'ocr-vs-adobe-scan', i18nKey: 'footer.comparisons.ocrVsAdobeScan', isComparison: true },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CATEGORY_KEYS: Record<string, string> = {
|
||||||
|
'PDF': 'footer.categories.pdf',
|
||||||
|
'Image & Convert': 'footer.categories.imageConvert',
|
||||||
|
'AI & Utility': 'footer.categories.aiUtility',
|
||||||
|
'Guides': 'footer.categories.guides',
|
||||||
|
'Comparisons': 'footer.categories.comparisons',
|
||||||
|
};
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -63,16 +78,13 @@ export default function Footer() {
|
|||||||
{t('common.appName')}
|
{t('common.appName')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
{t('common.siteTagline', 'Online PDF and file workflows')}
|
{t('common.siteTagline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-6 max-w-md text-sm leading-7 text-slate-600 dark:text-slate-300">
|
<p className="mt-6 max-w-md text-sm leading-7 text-slate-600 dark:text-slate-300">
|
||||||
{t(
|
{t('common.footerDescription')}
|
||||||
'common.footerDescription',
|
|
||||||
'Convert, compress, edit, and automate document work in one browser-based workspace built for speed, clarity, and secure processing.'
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 flex flex-wrap gap-3">
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
@@ -96,16 +108,16 @@ export default function Footer() {
|
|||||||
{Object.entries(FOOTER_TOOLS).map(([category, tools]) => (
|
{Object.entries(FOOTER_TOOLS).map(([category, tools]) => (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400">
|
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400">
|
||||||
{category}
|
{t(CATEGORY_KEYS[category] ?? category)}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-2.5">
|
<ul className="space-y-2.5">
|
||||||
{tools.map((tool) => (
|
{tools.map((tool) => (
|
||||||
<li key={tool.slug}>
|
<li key={tool.slug}>
|
||||||
<Link
|
<Link
|
||||||
to={(tool as { slug: string; isLanding?: boolean; isComparison?: boolean }).isComparison ? `/compare/${tool.slug}` : (tool as { slug: string; isLanding?: boolean }).isLanding ? `/${tool.slug}` : `/tools/${tool.slug}`}
|
to={tool.isComparison ? `/compare/${tool.slug}` : tool.isLanding ? `/${tool.slug}` : `/tools/${tool.slug}`}
|
||||||
className="text-sm text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
|
className="text-sm text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
|
||||||
>
|
>
|
||||||
{tool.label}
|
{t(tool.i18nKey)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default function Header() {
|
|||||||
{t('common.appName')}
|
{t('common.appName')}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden text-xs font-medium text-slate-500 dark:text-slate-400 sm:block">
|
<span className="hidden text-xs font-medium text-slate-500 dark:text-slate-400 sm:block">
|
||||||
{t('common.siteTagline', 'Online PDF and file workflows')}
|
{t('common.siteTagline')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -145,7 +145,7 @@ export default function Header() {
|
|||||||
className="hidden items-center gap-2 rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:-translate-y-0.5 hover:bg-primary-600 lg:inline-flex dark:bg-white dark:text-slate-950 dark:hover:bg-primary-300"
|
className="hidden items-center gap-2 rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:-translate-y-0.5 hover:bg-primary-600 lg:inline-flex dark:bg-white dark:text-slate-950 dark:hover:bg-primary-300"
|
||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
{t('home.startFree', 'Start Free')}
|
{t('home.startFree')}
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Component, type ReactNode } from 'react';
|
import { Component, type ReactNode } from 'react';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import { withTranslation, type WithTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends WithTranslation {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
fallbackMessage?: string;
|
fallbackMessage?: string;
|
||||||
}
|
}
|
||||||
@@ -10,7 +11,7 @@ interface State {
|
|||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ErrorBoundary extends Component<Props, State> {
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
state: State = { hasError: false };
|
state: State = { hasError: false };
|
||||||
|
|
||||||
static getDerivedStateFromError(): State {
|
static getDerivedStateFromError(): State {
|
||||||
@@ -22,6 +23,7 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-lg py-16 text-center">
|
<div className="mx-auto max-w-lg py-16 text-center">
|
||||||
@@ -29,16 +31,16 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
<AlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
<AlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mb-2 text-xl font-semibold text-slate-800 dark:text-slate-200">
|
<h2 className="mb-2 text-xl font-semibold text-slate-800 dark:text-slate-200">
|
||||||
{this.props.fallbackMessage || 'Something went wrong'}
|
{this.props.fallbackMessage || t('common.errors.genericTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
||||||
An unexpected error occurred. Please try again.
|
{t('common.errors.genericDesc')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={this.handleReset}
|
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"
|
className="rounded-lg bg-primary-600 px-6 py-2 text-sm font-medium text-white hover:bg-primary-700 transition-colors"
|
||||||
>
|
>
|
||||||
Try Again
|
{t('common.errors.tryAgain')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -46,3 +48,5 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withTranslation()(ErrorBoundary);
|
||||||
|
|||||||
@@ -172,10 +172,10 @@ export default function ToolTemplate({ config, onGetExtraData, children }: ToolT
|
|||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<>
|
<>
|
||||||
<Clock className="h-5 w-5 animate-spin" />
|
<Clock className="h-5 w-5 animate-spin" />
|
||||||
{t('common.uploading', { defaultValue: 'Uploading...' })}
|
{t('common.uploading')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
t('common.convert', { defaultValue: 'Convert' })
|
t('common.convert')
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,8 +195,8 @@ export default function ToolTemplate({ config, onGetExtraData, children }: ToolT
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
|
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold text-green-900 dark:text-green-200">Success!</h2>
|
<h2 className="font-semibold text-green-900 dark:text-green-200">{t('result.success')}</h2>
|
||||||
<p className="text-sm text-green-700 dark:text-green-300">Your file is ready</p>
|
<p className="text-sm text-green-700 dark:text-green-300">{t('result.fileReady')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,15 +208,15 @@ export default function ToolTemplate({ config, onGetExtraData, children }: ToolT
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400" />
|
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold text-red-900 dark:text-red-200">Error</h2>
|
<h2 className="font-semibold text-red-900 dark:text-red-200">{t('common.error')}</h2>
|
||||||
<p className="text-sm text-red-700 dark:text-red-300">{error || 'Processing failed'}</p>
|
<p className="text-sm text-red-700 dark:text-red-300">{error || t('common.errors.processingFailed')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button onClick={handleReset} className="btn-secondary w-full">
|
<button onClick={handleReset} className="btn-secondary w-full">
|
||||||
Process Another
|
{t('result.processAnother')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default function BarcodeGenerator() {
|
|||||||
{phase === 'done' && downloadUrl && (
|
{phase === 'done' && downloadUrl && (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className="rounded-2xl bg-white p-6 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
<div className="rounded-2xl bg-white p-6 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||||
<img src={downloadUrl} alt="Barcode" loading="lazy" decoding="async" className="mx-auto max-w-full" width="300" height="100" style={{aspectRatio:'3/1'}} />
|
<img src={downloadUrl} alt={t('tools.barcode.altText')} loading="lazy" decoding="async" className="mx-auto max-w-full" width="300" height="100" style={{aspectRatio:'3/1'}} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<a href={downloadUrl} download className="btn-primary flex-1">{t('common.download')}</a>
|
<a href={downloadUrl} download className="btn-primary flex-1">{t('common.download')}</a>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export default function ImageResize() {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="10000"
|
max="10000"
|
||||||
placeholder="e.g. 800"
|
placeholder={t('tools.imageResize.widthPlaceholder')}
|
||||||
value={width}
|
value={width}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setWidth(e.target.value);
|
setWidth(e.target.value);
|
||||||
@@ -160,7 +160,7 @@ export default function ImageResize() {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="10000"
|
max="10000"
|
||||||
placeholder="e.g. 600"
|
placeholder={t('tools.imageResize.heightPlaceholder')}
|
||||||
value={height}
|
value={height}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setHeight(e.target.value);
|
setHeight(e.target.value);
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function QrCodeGenerator() {
|
|||||||
{phase === 'done' && result && result.status === 'completed' && downloadUrl && (
|
{phase === 'done' && result && result.status === 'completed' && downloadUrl && (
|
||||||
<div className="space-y-6 text-center">
|
<div className="space-y-6 text-center">
|
||||||
<div className="rounded-2xl bg-white p-8 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
<div className="rounded-2xl bg-white p-8 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||||
<img src={downloadUrl} alt="QR Code" loading="lazy" decoding="async" className="mx-auto max-w-[300px] rounded-lg" width={size} height={size} style={{aspectRatio:'1/1'}} />
|
<img src={downloadUrl} alt={t('tools.qrCode.altText')} loading="lazy" decoding="async" className="mx-auto max-w-[300px] rounded-lg" width={size} height={size} style={{aspectRatio:'1/1'}} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<a href={downloadUrl} download={result.filename || 'qrcode.png'}
|
<a href={downloadUrl} download={result.filename || 'qrcode.png'}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"appName": "Dociva",
|
"appName": "Dociva",
|
||||||
"tagline": "أدوات ملفات مجانية على الإنترنت",
|
"tagline": "أدوات ملفات مجانية على الإنترنت",
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
"subject": "الموضوع",
|
"subject": "الموضوع",
|
||||||
"message": "الرسالة",
|
"message": "الرسالة",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
|
"siteTagline": "سير عمل PDF والملفات عبر الإنترنت",
|
||||||
|
"footerDescription": "حوّل، اضغط، عدّل، وأتمت عمل المستندات في مساحة عمل واحدة تعمل في المتصفح، مصممة للسرعة والوضوح والمعالجة الآمنة.",
|
||||||
|
"uploading": "جارٍ الرفع...",
|
||||||
|
"convert": "تحويل",
|
||||||
|
"sending": "جارٍ الإرسال...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"fileTooLarge": "حجم الملف كبير جدًا. الحد الأقصى المسموح {{size}} ميجابايت.",
|
"fileTooLarge": "حجم الملف كبير جدًا. الحد الأقصى المسموح {{size}} ميجابايت.",
|
||||||
"invalidFileType": "نوع الملف غير صالح. الأنواع المقبولة: {{types}}",
|
"invalidFileType": "نوع الملف غير صالح. الأنواع المقبولة: {{types}}",
|
||||||
@@ -44,7 +49,19 @@
|
|||||||
"rateLimited": "طلبات كثيرة جدًا. يرجى الانتظار لحظة والمحاولة مجددًا.",
|
"rateLimited": "طلبات كثيرة جدًا. يرجى الانتظار لحظة والمحاولة مجددًا.",
|
||||||
"serverError": "حدث خطأ في الخادم. يرجى المحاولة لاحقًا.",
|
"serverError": "حدث خطأ في الخادم. يرجى المحاولة لاحقًا.",
|
||||||
"networkError": "خطأ في الشبكة. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
|
"networkError": "خطأ في الشبكة. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
|
||||||
"noFileSelected": "لم يتم اختيار ملف. يرجى اختيار ملف للرفع."
|
"noFileSelected": "لم يتم اختيار ملف. يرجى اختيار ملف للرفع.",
|
||||||
|
"aiUnavailable": "ميزات الذكاء الاصطناعي غير متاحة مؤقتاً. يرجى المحاولة لاحقاً.",
|
||||||
|
"aiRateLimited": "خدمة الذكاء الاصطناعي مشغولة حالياً. يرجى المحاولة بعد قليل.",
|
||||||
|
"aiBudgetExceeded": "تم استنفاد حصة معالجة الذكاء الاصطناعي. يرجى المحاولة لاحقاً.",
|
||||||
|
"pdfEncrypted": "هذا الـ PDF محمي بكلمة مرور. يرجى إلغاء قفله أولاً.",
|
||||||
|
"pdfTextEmpty": "لم يُعثر على نص قابل للقراءة في هذا الـ PDF.",
|
||||||
|
"pdfNoTables": "لم تُعثر على جداول في هذا الـ PDF.",
|
||||||
|
"taskUnavailable": "الخدمة غير متاحة مؤقتاً. يرجى إعادة المحاولة بعد لحظة.",
|
||||||
|
"translationFailed": "فشلت خدمة الترجمة. يرجى المحاولة مرة أخرى.",
|
||||||
|
"invalidInput": "مدخلات غير صالحة. يرجى التحقق من إعداداتك والمحاولة مرة أخرى.",
|
||||||
|
"genericTitle": "حدث خطأ ما",
|
||||||
|
"genericDesc": "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.",
|
||||||
|
"tryAgain": "حاول مرة أخرى"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -141,7 +158,31 @@
|
|||||||
"feature2Title": "دقة يمكنك الوثوق بها",
|
"feature2Title": "دقة يمكنك الوثوق بها",
|
||||||
"feature2Desc": "احصل على ملفات دقيقة وقابلة للتعديل في ثوانٍ بدون فقدان للجودة.",
|
"feature2Desc": "احصل على ملفات دقيقة وقابلة للتعديل في ثوانٍ بدون فقدان للجودة.",
|
||||||
"feature3Title": "أمان مدمج",
|
"feature3Title": "أمان مدمج",
|
||||||
"feature3Desc": "قم بالوصول إلى ملفاتك بأمان، محمية بتشفير تلقائي."
|
"feature3Desc": "قم بالوصول إلى ملفاتك بأمان، محمية بتشفير تلقائي.",
|
||||||
|
"startFree": "ابدأ مجاناً",
|
||||||
|
"heroBadge": "سير عمل مستندات حديثة",
|
||||||
|
"statsToolsLabel": "إجمالي الأدوات",
|
||||||
|
"statsPdfLabel": "سير عمل PDF",
|
||||||
|
"statsOtherLabel": "الصور والذكاء الاصطناعي والأدوات",
|
||||||
|
"statsAccessLabel": "نموذج الوصول",
|
||||||
|
"statsAccessValue": "بدون تسجيل",
|
||||||
|
"trustSecure": "حذف تلقائي للملفات",
|
||||||
|
"trustFast": "نتائج في ثوانٍ",
|
||||||
|
"trust30Tools": "أكثر من 30 أداة مجانية",
|
||||||
|
"trustNoSignup": "لا حاجة للتسجيل",
|
||||||
|
"ctaBrowseTools": "تصفح كل الأدوات",
|
||||||
|
"quickStartLabel": "نقاط انطلاق شائعة",
|
||||||
|
"heroUploadEyebrow": "ارفع وابدأ",
|
||||||
|
"heroUploadTitle": "اختر ملفاً وانطلق مباشرة إلى الأداة المناسبة",
|
||||||
|
"howItWorksLabel": "عملية بسيطة",
|
||||||
|
"howItWorksTitle": "حوّل وعدّل في ثلاث خطوات بسيطة",
|
||||||
|
"toolsDirectoryTitle": "اعثر على الأداة المناسبة بسرعة أكبر",
|
||||||
|
"otherTools": "أدوات أخرى",
|
||||||
|
"whyChooseLabel": "لماذا Dociva",
|
||||||
|
"ctaBannerLabel": "ابدأ اليوم",
|
||||||
|
"ctaBannerTitle": "هل أنت مستعد لتحويل ملفاتك؟",
|
||||||
|
"ctaBannerSubtitle": "انضم إلى آلاف المستخدمين الذين يحوّلون ويضغطون ويعدّلون ملفاتهم يومياً — مجاناً تماماً.",
|
||||||
|
"ctaCreateAccount": "إنشاء حساب مجاني"
|
||||||
},
|
},
|
||||||
"socialProof": {
|
"socialProof": {
|
||||||
"badge": "موثوق من فرق نشطة",
|
"badge": "موثوق من فرق نشطة",
|
||||||
@@ -173,7 +214,11 @@
|
|||||||
"أدوات المستندات بالذكاء الاصطناعي — التحدث مع PDF، التلخيص، الترجمة، استخراج الجداول",
|
"أدوات المستندات بالذكاء الاصطناعي — التحدث مع PDF، التلخيص، الترجمة، استخراج الجداول",
|
||||||
"OCR — استخراج النص من الصور وملفات PDF الممسوحة ضوئياً بالعربية والإنجليزية والفرنسية",
|
"OCR — استخراج النص من الصور وملفات PDF الممسوحة ضوئياً بالعربية والإنجليزية والفرنسية",
|
||||||
"أدوات مساعدة — مولد QR، تحويل فيديو إلى GIF، عداد الكلمات، منظف النصوص"
|
"أدوات مساعدة — مولد QR، تحويل فيديو إلى GIF، عداد الكلمات، منظف النصوص"
|
||||||
]
|
],
|
||||||
|
"heroTitle": "تمكين إنتاجية المستندات في كل مكان",
|
||||||
|
"teamTitle": "فريقنا",
|
||||||
|
"valuesTitle": "قيمنا",
|
||||||
|
"ctaText": "هل لديك أسئلة؟ تواصل معنا."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"metaDescription": "تواصل مع فريق Dociva. أبلغ عن خطأ أو اطلب ميزة جديدة أو أرسل لنا رسالة.",
|
"metaDescription": "تواصل مع فريق Dociva. أبلغ عن خطأ أو اطلب ميزة جديدة أو أرسل لنا رسالة.",
|
||||||
@@ -194,7 +239,12 @@
|
|||||||
"subjectPlaceholder": "الموضوع",
|
"subjectPlaceholder": "الموضوع",
|
||||||
"successMessage": "تم إرسال رسالتك! سنرد عليك قريباً.",
|
"successMessage": "تم إرسال رسالتك! سنرد عليك قريباً.",
|
||||||
"directEmail": "أو راسلنا مباشرة على",
|
"directEmail": "أو راسلنا مباشرة على",
|
||||||
"responseTime": "نرد عادةً خلال 24-48 ساعة."
|
"responseTime": "نرد عادةً خلال 24-48 ساعة.",
|
||||||
|
"emailLabel": "البريد الإلكتروني:",
|
||||||
|
"phoneLabel": "الهاتف:",
|
||||||
|
"officeLabel": "المكتب:",
|
||||||
|
"connectTitle": "تواصل معنا",
|
||||||
|
"faqTitle": "الأسئلة الشائعة"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"metaDescription": "سياسة الخصوصية لـ Dociva. تعرّف على كيفية تعاملنا مع ملفاتك وبياناتك بشفافية كاملة.",
|
"metaDescription": "سياسة الخصوصية لـ Dociva. تعرّف على كيفية تعاملنا مع ملفاتك وبياناتك بشفافية كاملة.",
|
||||||
@@ -643,7 +693,9 @@
|
|||||||
"height": "الارتفاع (بكسل)",
|
"height": "الارتفاع (بكسل)",
|
||||||
"quality": "الجودة",
|
"quality": "الجودة",
|
||||||
"lockAspect": "قفل نسبة العرض للارتفاع",
|
"lockAspect": "قفل نسبة العرض للارتفاع",
|
||||||
"aspectHint": "أدخل بُعداً واحداً — سيتم حساب الآخر تلقائياً للحفاظ على نسبة العرض للارتفاع."
|
"aspectHint": "أدخل بُعداً واحداً — سيتم حساب الآخر تلقائياً للحفاظ على نسبة العرض للارتفاع.",
|
||||||
|
"widthPlaceholder": "مثال: 800",
|
||||||
|
"heightPlaceholder": "مثال: 600"
|
||||||
},
|
},
|
||||||
"imageToSvg": {
|
"imageToSvg": {
|
||||||
"title": "تحويل الصورة إلى SVG",
|
"title": "تحويل الصورة إلى SVG",
|
||||||
@@ -989,7 +1041,8 @@
|
|||||||
"shortDesc": "إنشاء رمز QR",
|
"shortDesc": "إنشاء رمز QR",
|
||||||
"dataLabel": "نص أو رابط",
|
"dataLabel": "نص أو رابط",
|
||||||
"dataPlaceholder": "أدخل نصاً أو رابطاً أو أي بيانات...",
|
"dataPlaceholder": "أدخل نصاً أو رابطاً أو أي بيانات...",
|
||||||
"sizeLabel": "الحجم"
|
"sizeLabel": "الحجم",
|
||||||
|
"altText": "رمز QR المُولَّد"
|
||||||
},
|
},
|
||||||
"htmlToPdf": {
|
"htmlToPdf": {
|
||||||
"title": "HTML إلى PDF",
|
"title": "HTML إلى PDF",
|
||||||
@@ -1124,7 +1177,29 @@
|
|||||||
"dataLabel": "بيانات الباركود",
|
"dataLabel": "بيانات الباركود",
|
||||||
"dataPlaceholder": "أدخل البيانات للترميز...",
|
"dataPlaceholder": "أدخل البيانات للترميز...",
|
||||||
"typeLabel": "نوع الباركود",
|
"typeLabel": "نوع الباركود",
|
||||||
"formatLabel": "تنسيق الإخراج"
|
"formatLabel": "تنسيق الإخراج",
|
||||||
|
"altText": "باركود مُنشأ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"categories": {
|
||||||
|
"pdf": "PDF",
|
||||||
|
"imageConvert": "الصور والتحويل",
|
||||||
|
"aiUtility": "الذكاء الاصطناعي والأدوات",
|
||||||
|
"guides": "الأدلة",
|
||||||
|
"comparisons": "المقارنات"
|
||||||
|
},
|
||||||
|
"guides": {
|
||||||
|
"bestPdfTools": "أفضل أدوات PDF",
|
||||||
|
"freePdfToolsOnline": "أدوات PDF مجانية عبر الإنترنت",
|
||||||
|
"convertFilesOnline": "تحويل الملفات عبر الإنترنت"
|
||||||
|
},
|
||||||
|
"comparisons": {
|
||||||
|
"compressPdfVsIlovepdf": "Dociva مقابل iLovePDF",
|
||||||
|
"mergePdfVsSmallpdf": "Dociva مقابل Smallpdf",
|
||||||
|
"pdfToWordVsAdobeAcrobat": "Dociva مقابل Adobe Acrobat",
|
||||||
|
"compressImageVsTinypng": "Dociva مقابل TinyPNG",
|
||||||
|
"ocrVsAdobeScan": "Dociva مقابل Adobe Scan"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
@@ -1208,7 +1283,10 @@
|
|||||||
"newSize": "الحجم الجديد",
|
"newSize": "الحجم الجديد",
|
||||||
"reduction": "نسبة التقليل",
|
"reduction": "نسبة التقليل",
|
||||||
"downloadReady": "ملفك جاهز للتحميل.",
|
"downloadReady": "ملفك جاهز للتحميل.",
|
||||||
"linkExpiry": "رابط التحميل ينتهي خلال 30 دقيقة."
|
"linkExpiry": "رابط التحميل ينتهي خلال 30 دقيقة.",
|
||||||
|
"success": "تم بنجاح!",
|
||||||
|
"fileReady": "ملفك جاهز",
|
||||||
|
"processAnother": "معالجة ملف آخر"
|
||||||
},
|
},
|
||||||
"downloadGate": {
|
"downloadGate": {
|
||||||
"title": "سجّل لتحميل ملفك",
|
"title": "سجّل لتحميل ملفك",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"appName": "Dociva",
|
"appName": "Dociva",
|
||||||
"tagline": "Free Online File Tools",
|
"tagline": "Free Online File Tools",
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
"subject": "Subject",
|
"subject": "Subject",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"siteTagline": "Online PDF and file workflows",
|
||||||
|
"footerDescription": "Convert, compress, edit, and automate document work in one browser-based workspace built for speed, clarity, and secure processing.",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"convert": "Convert",
|
||||||
|
"sending": "Sending...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"fileTooLarge": "File is too large. Maximum size is {{size}}MB.",
|
"fileTooLarge": "File is too large. Maximum size is {{size}}MB.",
|
||||||
"invalidFileType": "Invalid file type. Accepted: {{types}}",
|
"invalidFileType": "Invalid file type. Accepted: {{types}}",
|
||||||
@@ -44,7 +49,19 @@
|
|||||||
"rateLimited": "Too many requests. Please wait a moment and try again.",
|
"rateLimited": "Too many requests. Please wait a moment and try again.",
|
||||||
"serverError": "A server error occurred. Please try again later.",
|
"serverError": "A server error occurred. Please try again later.",
|
||||||
"networkError": "Network error. Please check your connection and try again.",
|
"networkError": "Network error. Please check your connection and try again.",
|
||||||
"noFileSelected": "No file selected. Please choose a file to upload."
|
"noFileSelected": "No file selected. Please choose a file to upload.",
|
||||||
|
"aiUnavailable": "AI features are temporarily unavailable. Please try again later.",
|
||||||
|
"aiRateLimited": "AI service is currently busy. Please try again shortly.",
|
||||||
|
"aiBudgetExceeded": "AI processing quota exceeded. Please try again later.",
|
||||||
|
"pdfEncrypted": "This PDF is password-protected. Please unlock it first.",
|
||||||
|
"pdfTextEmpty": "No readable text found in this PDF.",
|
||||||
|
"pdfNoTables": "No tables found in this PDF.",
|
||||||
|
"taskUnavailable": "Service temporarily unavailable. Please retry in a moment.",
|
||||||
|
"translationFailed": "Translation service failed. Please try again.",
|
||||||
|
"invalidInput": "Invalid input. Please check your settings and try again.",
|
||||||
|
"genericTitle": "Something went wrong",
|
||||||
|
"genericDesc": "An unexpected error occurred. Please try again.",
|
||||||
|
"tryAgain": "Try Again"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -141,7 +158,31 @@
|
|||||||
"feature2Title": "Accuracy you can trust",
|
"feature2Title": "Accuracy you can trust",
|
||||||
"feature2Desc": "Get pixel-perfect, editable files in seconds with zero quality loss.",
|
"feature2Desc": "Get pixel-perfect, editable files in seconds with zero quality loss.",
|
||||||
"feature3Title": "Built-in security",
|
"feature3Title": "Built-in security",
|
||||||
"feature3Desc": "Access files securely, protected by automatic encryption."
|
"feature3Desc": "Access files securely, protected by automatic encryption.",
|
||||||
|
"startFree": "Start Free",
|
||||||
|
"heroBadge": "Modern document workflows",
|
||||||
|
"statsToolsLabel": "Total tools",
|
||||||
|
"statsPdfLabel": "PDF workflows",
|
||||||
|
"statsOtherLabel": "Image, AI & utility",
|
||||||
|
"statsAccessLabel": "Access model",
|
||||||
|
"statsAccessValue": "No signup",
|
||||||
|
"trustSecure": "Files auto-deleted",
|
||||||
|
"trustFast": "Results in seconds",
|
||||||
|
"trust30Tools": "30+ free tools",
|
||||||
|
"trustNoSignup": "No sign-up needed",
|
||||||
|
"ctaBrowseTools": "Browse All Tools",
|
||||||
|
"quickStartLabel": "Popular starting points",
|
||||||
|
"heroUploadEyebrow": "Upload and start",
|
||||||
|
"heroUploadTitle": "Choose a file and jump straight into the right tool",
|
||||||
|
"howItWorksLabel": "Simple process",
|
||||||
|
"howItWorksTitle": "Convert and edit in three simple steps",
|
||||||
|
"toolsDirectoryTitle": "Find the right tool faster",
|
||||||
|
"otherTools": "Other Tools",
|
||||||
|
"whyChooseLabel": "Why Dociva",
|
||||||
|
"ctaBannerLabel": "Get started today",
|
||||||
|
"ctaBannerTitle": "Ready to convert your files?",
|
||||||
|
"ctaBannerSubtitle": "Join thousands of users who convert, compress, and edit their files every day — completely free.",
|
||||||
|
"ctaCreateAccount": "Create Free Account"
|
||||||
},
|
},
|
||||||
"socialProof": {
|
"socialProof": {
|
||||||
"badge": "Trusted by active teams",
|
"badge": "Trusted by active teams",
|
||||||
@@ -173,7 +214,11 @@
|
|||||||
"AI document tools — chat with PDFs, summarize, translate, extract tables",
|
"AI document tools — chat with PDFs, summarize, translate, extract tables",
|
||||||
"OCR — extract text from images and scanned PDFs in English, Arabic, and French",
|
"OCR — extract text from images and scanned PDFs in English, Arabic, and French",
|
||||||
"Utility tools — QR code generator, video to GIF, word counter, text cleaner"
|
"Utility tools — QR code generator, video to GIF, word counter, text cleaner"
|
||||||
]
|
],
|
||||||
|
"heroTitle": "Empowering Document Productivity Worldwide",
|
||||||
|
"teamTitle": "Our Team",
|
||||||
|
"valuesTitle": "Our Values",
|
||||||
|
"ctaText": "Have questions? Get in touch."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"metaDescription": "Contact the Dociva team. Report bugs, request features, or send us a message.",
|
"metaDescription": "Contact the Dociva team. Report bugs, request features, or send us a message.",
|
||||||
@@ -194,7 +239,12 @@
|
|||||||
"subjectPlaceholder": "Subject",
|
"subjectPlaceholder": "Subject",
|
||||||
"successMessage": "Your message has been sent! We'll get back to you soon.",
|
"successMessage": "Your message has been sent! We'll get back to you soon.",
|
||||||
"directEmail": "Or email us directly at",
|
"directEmail": "Or email us directly at",
|
||||||
"responseTime": "We typically respond within 24–48 hours."
|
"responseTime": "We typically respond within 24–48 hours.",
|
||||||
|
"emailLabel": "Email:",
|
||||||
|
"phoneLabel": "Phone:",
|
||||||
|
"officeLabel": "Office:",
|
||||||
|
"connectTitle": "Connect With Us",
|
||||||
|
"faqTitle": "FAQ"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"metaDescription": "Privacy policy for Dociva. Learn how we handle your files and data with full transparency.",
|
"metaDescription": "Privacy policy for Dociva. Learn how we handle your files and data with full transparency.",
|
||||||
@@ -643,7 +693,9 @@
|
|||||||
"height": "Height (px)",
|
"height": "Height (px)",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
"lockAspect": "Lock aspect ratio",
|
"lockAspect": "Lock aspect ratio",
|
||||||
"aspectHint": "Enter one dimension — the other will auto-calculate to preserve aspect ratio."
|
"aspectHint": "Enter one dimension — the other will auto-calculate to preserve aspect ratio.",
|
||||||
|
"widthPlaceholder": "e.g. 800",
|
||||||
|
"heightPlaceholder": "e.g. 600"
|
||||||
},
|
},
|
||||||
"imageToSvg": {
|
"imageToSvg": {
|
||||||
"title": "Image to SVG",
|
"title": "Image to SVG",
|
||||||
@@ -989,7 +1041,8 @@
|
|||||||
"shortDesc": "Generate QR Code",
|
"shortDesc": "Generate QR Code",
|
||||||
"dataLabel": "Text or URL",
|
"dataLabel": "Text or URL",
|
||||||
"dataPlaceholder": "Enter text, URL, or any data...",
|
"dataPlaceholder": "Enter text, URL, or any data...",
|
||||||
"sizeLabel": "Size"
|
"sizeLabel": "Size",
|
||||||
|
"altText": "Generated QR Code"
|
||||||
},
|
},
|
||||||
"htmlToPdf": {
|
"htmlToPdf": {
|
||||||
"title": "HTML to PDF",
|
"title": "HTML to PDF",
|
||||||
@@ -1124,7 +1177,29 @@
|
|||||||
"dataLabel": "Barcode Data",
|
"dataLabel": "Barcode Data",
|
||||||
"dataPlaceholder": "Enter data to encode...",
|
"dataPlaceholder": "Enter data to encode...",
|
||||||
"typeLabel": "Barcode Type",
|
"typeLabel": "Barcode Type",
|
||||||
"formatLabel": "Output Format"
|
"formatLabel": "Output Format",
|
||||||
|
"altText": "Generated barcode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"categories": {
|
||||||
|
"pdf": "PDF",
|
||||||
|
"imageConvert": "Image & Convert",
|
||||||
|
"aiUtility": "AI & Utility",
|
||||||
|
"guides": "Guides",
|
||||||
|
"comparisons": "Comparisons"
|
||||||
|
},
|
||||||
|
"guides": {
|
||||||
|
"bestPdfTools": "Best PDF Tools",
|
||||||
|
"freePdfToolsOnline": "Free PDF Tools Online",
|
||||||
|
"convertFilesOnline": "Convert Files Online"
|
||||||
|
},
|
||||||
|
"comparisons": {
|
||||||
|
"compressPdfVsIlovepdf": "Dociva vs iLovePDF",
|
||||||
|
"mergePdfVsSmallpdf": "Dociva vs Smallpdf",
|
||||||
|
"pdfToWordVsAdobeAcrobat": "Dociva vs Adobe Acrobat",
|
||||||
|
"compressImageVsTinypng": "Dociva vs TinyPNG",
|
||||||
|
"ocrVsAdobeScan": "Dociva vs Adobe Scan"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
@@ -1208,7 +1283,10 @@
|
|||||||
"newSize": "New Size",
|
"newSize": "New Size",
|
||||||
"reduction": "Reduction",
|
"reduction": "Reduction",
|
||||||
"downloadReady": "Your file is ready for download.",
|
"downloadReady": "Your file is ready for download.",
|
||||||
"linkExpiry": "Download link expires in 30 minutes."
|
"linkExpiry": "Download link expires in 30 minutes.",
|
||||||
|
"success": "Success!",
|
||||||
|
"fileReady": "Your file is ready",
|
||||||
|
"processAnother": "Process Another"
|
||||||
},
|
},
|
||||||
"downloadGate": {
|
"downloadGate": {
|
||||||
"title": "Sign up to download your file",
|
"title": "Sign up to download your file",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"appName": "Dociva",
|
"appName": "Dociva",
|
||||||
"tagline": "Outils de fichiers en ligne gratuits",
|
"tagline": "Outils de fichiers en ligne gratuits",
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
"subject": "Sujet",
|
"subject": "Sujet",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
|
"siteTagline": "Workflows PDF et fichiers en ligne",
|
||||||
|
"footerDescription": "Convertissez, compressez, modifiez et automatisez le traitement de documents dans un espace de travail basé sur le navigateur, conçu pour la rapidité, la clarté et le traitement sécurisé.",
|
||||||
|
"uploading": "Téléchargement en cours...",
|
||||||
|
"convert": "Convertir",
|
||||||
|
"sending": "Envoi en cours...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"fileTooLarge": "Fichier trop volumineux. Taille maximale autorisée : {{size}} Mo.",
|
"fileTooLarge": "Fichier trop volumineux. Taille maximale autorisée : {{size}} Mo.",
|
||||||
"invalidFileType": "Type de fichier non valide. Formats acceptés : {{types}}",
|
"invalidFileType": "Type de fichier non valide. Formats acceptés : {{types}}",
|
||||||
@@ -44,7 +49,19 @@
|
|||||||
"rateLimited": "Trop de requêtes. Veuillez attendre un moment et réessayer.",
|
"rateLimited": "Trop de requêtes. Veuillez attendre un moment et réessayer.",
|
||||||
"serverError": "Une erreur serveur s'est produite. Veuillez réessayer plus tard.",
|
"serverError": "Une erreur serveur s'est produite. Veuillez réessayer plus tard.",
|
||||||
"networkError": "Erreur réseau. Veuillez vérifier votre connexion et réessayer.",
|
"networkError": "Erreur réseau. Veuillez vérifier votre connexion et réessayer.",
|
||||||
"noFileSelected": "Aucun fichier sélectionné. Veuillez choisir un fichier à télécharger."
|
"noFileSelected": "Aucun fichier sélectionné. Veuillez choisir un fichier à télécharger.",
|
||||||
|
"aiUnavailable": "Les fonctionnalités IA sont temporairement indisponibles. Veuillez réessayer plus tard.",
|
||||||
|
"aiRateLimited": "Le service IA est actuellement occupé. Veuillez réessayer dans un instant.",
|
||||||
|
"aiBudgetExceeded": "Quota de traitement IA dépassé. Veuillez réessayer plus tard.",
|
||||||
|
"pdfEncrypted": "Ce PDF est protégé par un mot de passe. Veuillez d'abord le déverrouiller.",
|
||||||
|
"pdfTextEmpty": "Aucun texte lisible trouvé dans ce PDF.",
|
||||||
|
"pdfNoTables": "Aucun tableau trouvé dans ce PDF.",
|
||||||
|
"taskUnavailable": "Service temporairement indisponible. Veuillez réessayer dans un instant.",
|
||||||
|
"translationFailed": "Le service de traduction a échoué. Veuillez réessayer.",
|
||||||
|
"invalidInput": "Entrée non valide. Veuillez vérifier vos paramètres et réessayer.",
|
||||||
|
"genericTitle": "Une erreur s'est produite",
|
||||||
|
"genericDesc": "Une erreur inattendue s'est produite. Veuillez réessayer.",
|
||||||
|
"tryAgain": "Réessayer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -141,7 +158,31 @@
|
|||||||
"feature2Title": "Une précision de confiance",
|
"feature2Title": "Une précision de confiance",
|
||||||
"feature2Desc": "Obtenez des fichiers parfaits et modifiables en quelques secondes sans perte de qualité.",
|
"feature2Desc": "Obtenez des fichiers parfaits et modifiables en quelques secondes sans perte de qualité.",
|
||||||
"feature3Title": "Sécurité intégrée",
|
"feature3Title": "Sécurité intégrée",
|
||||||
"feature3Desc": "Accédez aux fichiers en toute sécurité, protégés par un cryptage automatique."
|
"feature3Desc": "Accédez aux fichiers en toute sécurité, protégés par un cryptage automatique.",
|
||||||
|
"startFree": "Commencer gratuitement",
|
||||||
|
"heroBadge": "Workflows de documents modernes",
|
||||||
|
"statsToolsLabel": "Total des outils",
|
||||||
|
"statsPdfLabel": "Workflows PDF",
|
||||||
|
"statsOtherLabel": "Image, IA et utilitaires",
|
||||||
|
"statsAccessLabel": "Modèle d'accès",
|
||||||
|
"statsAccessValue": "Sans inscription",
|
||||||
|
"trustSecure": "Fichiers supprimés automatiquement",
|
||||||
|
"trustFast": "Résultats en quelques secondes",
|
||||||
|
"trust30Tools": "30+ outils gratuits",
|
||||||
|
"trustNoSignup": "Aucune inscription requise",
|
||||||
|
"ctaBrowseTools": "Parcourir tous les outils",
|
||||||
|
"quickStartLabel": "Points de départ populaires",
|
||||||
|
"heroUploadEyebrow": "Déposez et commencez",
|
||||||
|
"heroUploadTitle": "Choisissez un fichier et accédez directement au bon outil",
|
||||||
|
"howItWorksLabel": "Processus simple",
|
||||||
|
"howItWorksTitle": "Convertissez et modifiez en trois étapes simples",
|
||||||
|
"toolsDirectoryTitle": "Trouvez le bon outil plus rapidement",
|
||||||
|
"otherTools": "Autres outils",
|
||||||
|
"whyChooseLabel": "Pourquoi Dociva",
|
||||||
|
"ctaBannerLabel": "Commencez dès aujourd'hui",
|
||||||
|
"ctaBannerTitle": "Prêt à convertir vos fichiers ?",
|
||||||
|
"ctaBannerSubtitle": "Rejoignez des milliers d'utilisateurs qui convertissent, compressent et modifient leurs fichiers chaque jour — complètement gratuit.",
|
||||||
|
"ctaCreateAccount": "Créer un compte gratuit"
|
||||||
},
|
},
|
||||||
"socialProof": {
|
"socialProof": {
|
||||||
"badge": "Adopté par des équipes actives",
|
"badge": "Adopté par des équipes actives",
|
||||||
@@ -173,7 +214,11 @@
|
|||||||
"Outils documentaires IA — discuter avec des PDF, résumer, traduire, extraire des tableaux",
|
"Outils documentaires IA — discuter avec des PDF, résumer, traduire, extraire des tableaux",
|
||||||
"OCR — extraire du texte d'images et de PDF numérisés en anglais, arabe et français",
|
"OCR — extraire du texte d'images et de PDF numérisés en anglais, arabe et français",
|
||||||
"Outils utilitaires — générateur de QR code, vidéo vers GIF, compteur de mots, nettoyeur de texte"
|
"Outils utilitaires — générateur de QR code, vidéo vers GIF, compteur de mots, nettoyeur de texte"
|
||||||
]
|
],
|
||||||
|
"heroTitle": "Améliorer la productivité documentaire dans le monde entier",
|
||||||
|
"teamTitle": "Notre équipe",
|
||||||
|
"valuesTitle": "Nos valeurs",
|
||||||
|
"ctaText": "Des questions ? Contactez-nous."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"metaDescription": "Contactez l'équipe Dociva. Signalez un bug, demandez une fonctionnalité ou envoyez-nous un message.",
|
"metaDescription": "Contactez l'équipe Dociva. Signalez un bug, demandez une fonctionnalité ou envoyez-nous un message.",
|
||||||
@@ -194,7 +239,12 @@
|
|||||||
"subjectPlaceholder": "Sujet",
|
"subjectPlaceholder": "Sujet",
|
||||||
"successMessage": "Votre message a été envoyé ! Nous vous répondrons bientôt.",
|
"successMessage": "Votre message a été envoyé ! Nous vous répondrons bientôt.",
|
||||||
"directEmail": "Ou contactez-nous directement à",
|
"directEmail": "Ou contactez-nous directement à",
|
||||||
"responseTime": "Nous répondons généralement sous 24 à 48 heures."
|
"responseTime": "Nous répondons généralement sous 24 à 48 heures.",
|
||||||
|
"emailLabel": "E-mail :",
|
||||||
|
"phoneLabel": "Téléphone :",
|
||||||
|
"officeLabel": "Bureau :",
|
||||||
|
"connectTitle": "Connectez-vous avec nous",
|
||||||
|
"faqTitle": "FAQ"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"metaDescription": "Politique de confidentialité de Dociva. Découvrez comment nous gérons vos fichiers et données en toute transparence.",
|
"metaDescription": "Politique de confidentialité de Dociva. Découvrez comment nous gérons vos fichiers et données en toute transparence.",
|
||||||
@@ -643,7 +693,9 @@
|
|||||||
"height": "Hauteur (px)",
|
"height": "Hauteur (px)",
|
||||||
"quality": "Qualité",
|
"quality": "Qualité",
|
||||||
"lockAspect": "Verrouiller le rapport d'aspect",
|
"lockAspect": "Verrouiller le rapport d'aspect",
|
||||||
"aspectHint": "Entrez une dimension — l'autre sera calculée automatiquement pour préserver le rapport d'aspect."
|
"aspectHint": "Entrez une dimension — l'autre sera calculée automatiquement pour préserver le rapport d'aspect.",
|
||||||
|
"widthPlaceholder": "ex. 800",
|
||||||
|
"heightPlaceholder": "ex. 600"
|
||||||
},
|
},
|
||||||
"imageToSvg": {
|
"imageToSvg": {
|
||||||
"title": "Image vers SVG",
|
"title": "Image vers SVG",
|
||||||
@@ -989,7 +1041,8 @@
|
|||||||
"shortDesc": "Générer un code QR",
|
"shortDesc": "Générer un code QR",
|
||||||
"dataLabel": "Texte ou URL",
|
"dataLabel": "Texte ou URL",
|
||||||
"dataPlaceholder": "Entrez du texte, une URL ou des données...",
|
"dataPlaceholder": "Entrez du texte, une URL ou des données...",
|
||||||
"sizeLabel": "Taille"
|
"sizeLabel": "Taille",
|
||||||
|
"altText": "QR Code généré"
|
||||||
},
|
},
|
||||||
"htmlToPdf": {
|
"htmlToPdf": {
|
||||||
"title": "HTML vers PDF",
|
"title": "HTML vers PDF",
|
||||||
@@ -1124,7 +1177,29 @@
|
|||||||
"dataLabel": "Données du code-barres",
|
"dataLabel": "Données du code-barres",
|
||||||
"dataPlaceholder": "Entrez les données à encoder...",
|
"dataPlaceholder": "Entrez les données à encoder...",
|
||||||
"typeLabel": "Type de code-barres",
|
"typeLabel": "Type de code-barres",
|
||||||
"formatLabel": "Format de sortie"
|
"formatLabel": "Format de sortie",
|
||||||
|
"altText": "Code-barres généré"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"categories": {
|
||||||
|
"pdf": "PDF",
|
||||||
|
"imageConvert": "Image & Conversion",
|
||||||
|
"aiUtility": "IA & Utilitaires",
|
||||||
|
"guides": "Guides",
|
||||||
|
"comparisons": "Comparaisons"
|
||||||
|
},
|
||||||
|
"guides": {
|
||||||
|
"bestPdfTools": "Meilleurs outils PDF",
|
||||||
|
"freePdfToolsOnline": "Outils PDF gratuits en ligne",
|
||||||
|
"convertFilesOnline": "Convertir des fichiers en ligne"
|
||||||
|
},
|
||||||
|
"comparisons": {
|
||||||
|
"compressPdfVsIlovepdf": "Dociva vs iLovePDF",
|
||||||
|
"mergePdfVsSmallpdf": "Dociva vs Smallpdf",
|
||||||
|
"pdfToWordVsAdobeAcrobat": "Dociva vs Adobe Acrobat",
|
||||||
|
"compressImageVsTinypng": "Dociva vs TinyPNG",
|
||||||
|
"ocrVsAdobeScan": "Dociva vs Adobe Scan"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
@@ -1208,7 +1283,10 @@
|
|||||||
"newSize": "Nouvelle taille",
|
"newSize": "Nouvelle taille",
|
||||||
"reduction": "Réduction",
|
"reduction": "Réduction",
|
||||||
"downloadReady": "Votre fichier est prêt à être téléchargé.",
|
"downloadReady": "Votre fichier est prêt à être téléchargé.",
|
||||||
"linkExpiry": "Le lien de téléchargement expire dans 30 minutes."
|
"linkExpiry": "Le lien de téléchargement expire dans 30 minutes.",
|
||||||
|
"success": "Succès !",
|
||||||
|
"fileReady": "Votre fichier est prêt",
|
||||||
|
"processAnother": "Traiter un autre fichier"
|
||||||
},
|
},
|
||||||
"downloadGate": {
|
"downloadGate": {
|
||||||
"title": "Inscrivez-vous pour télécharger votre fichier",
|
"title": "Inscrivez-vous pour télécharger votre fichier",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios, { type InternalAxiosRequestConfig } from 'axios';
|
import axios, { type InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
const CSRF_COOKIE_NAME = 'csrf_token';
|
const CSRF_COOKIE_NAME = 'csrf_token';
|
||||||
const CSRF_HEADER_NAME = 'X-CSRF-Token';
|
const CSRF_HEADER_NAME = 'X-CSRF-Token';
|
||||||
@@ -160,21 +161,27 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error.response.status === 429) {
|
if (error.response.status === 429) {
|
||||||
return Promise.reject(new Error('Too many requests. Please wait a moment and try again.'));
|
return Promise.reject(new Error(i18n.t('common.errors.rateLimited')));
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = error.response.data;
|
const responseData = error.response.data;
|
||||||
|
const errorCode: string | undefined = responseData?.error_code;
|
||||||
|
if (errorCode) {
|
||||||
|
const mapped = resolveErrorCode(errorCode);
|
||||||
|
if (mapped) return Promise.reject(new Error(mapped));
|
||||||
|
}
|
||||||
const message =
|
const message =
|
||||||
|
responseData?.user_message ||
|
||||||
responseData?.error ||
|
responseData?.error ||
|
||||||
responseData?.message ||
|
responseData?.message ||
|
||||||
(typeof responseData === 'string' && responseData.trim()
|
(typeof responseData === 'string' && responseData.trim()
|
||||||
? responseData.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
? responseData.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||||
: null) ||
|
: null) ||
|
||||||
`Request failed (${error.response.status}).`;
|
i18n.t('common.errors.serverError');
|
||||||
return Promise.reject(new Error(message));
|
return Promise.reject(new Error(message));
|
||||||
}
|
}
|
||||||
if (error.request) {
|
if (error.request) {
|
||||||
return Promise.reject(new Error('Network error. Please check your connection.'));
|
return Promise.reject(new Error(i18n.t('common.errors.networkError')));
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@@ -251,7 +258,57 @@ function isTaskErrorPayload(value: unknown): value is TaskErrorPayload {
|
|||||||
return Boolean(value) && typeof value === 'object';
|
return Boolean(value) && typeof value === 'object';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a backend error_code to a fully translated message via i18n.
|
||||||
|
* Returns null when no specific mapping exists (caller should fall back to user_message or generic).
|
||||||
|
*/
|
||||||
|
export function resolveErrorCode(errorCode: string): string | null {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
TASK_FAILURE: i18n.t('common.errors.processingFailed'),
|
||||||
|
CELERY_NOT_REGISTERED: i18n.t('common.errors.taskUnavailable'),
|
||||||
|
OPENROUTER_UNAUTHORIZED: i18n.t('common.errors.aiUnavailable'),
|
||||||
|
OPENROUTER_RATE_LIMIT: i18n.t('common.errors.aiRateLimited'),
|
||||||
|
OPENROUTER_INSUFFICIENT_CREDITS: i18n.t('common.errors.aiRateLimited'),
|
||||||
|
OPENROUTER_SERVER_ERROR: i18n.t('common.errors.serverError'),
|
||||||
|
OPENROUTER_CONNECTION_ERROR: i18n.t('common.errors.networkError'),
|
||||||
|
OPENROUTER_TIMEOUT: i18n.t('common.errors.serverError'),
|
||||||
|
OPENROUTER_MISSING_API_KEY: i18n.t('common.errors.aiUnavailable'),
|
||||||
|
OPENROUTER_EMPTY_RESPONSE: i18n.t('common.errors.aiUnavailable'),
|
||||||
|
OPENROUTER_ERROR_PAYLOAD: i18n.t('common.errors.aiUnavailable'),
|
||||||
|
OPENROUTER_REQUEST_ERROR: i18n.t('common.errors.serverError'),
|
||||||
|
DEEPL_NOT_CONFIGURED: i18n.t('common.errors.translationFailed'),
|
||||||
|
DEEPL_UNSUPPORTED_TARGET_LANGUAGE: i18n.t('common.errors.invalidInput'),
|
||||||
|
DEEPL_TIMEOUT: i18n.t('common.errors.translationFailed'),
|
||||||
|
DEEPL_CONNECTION_ERROR: i18n.t('common.errors.networkError'),
|
||||||
|
DEEPL_REQUEST_ERROR: i18n.t('common.errors.translationFailed'),
|
||||||
|
DEEPL_RATE_LIMIT: i18n.t('common.errors.aiRateLimited'),
|
||||||
|
DEEPL_SERVER_ERROR: i18n.t('common.errors.serverError'),
|
||||||
|
DEEPL_CREDITS_OR_PERMISSIONS: i18n.t('common.errors.translationFailed'),
|
||||||
|
DEEPL_EMPTY_RESPONSE: i18n.t('common.errors.translationFailed'),
|
||||||
|
DEEPL_EMPTY_TEXT: i18n.t('common.errors.pdfTextEmpty'),
|
||||||
|
TRANSLATION_PROVIDER_FAILED: i18n.t('common.errors.translationFailed'),
|
||||||
|
AI_BUDGET_EXCEEDED: i18n.t('common.errors.aiBudgetExceeded'),
|
||||||
|
PDF_ENCRYPTED: i18n.t('common.errors.pdfEncrypted'),
|
||||||
|
PDF_TEXT_EXTRACTION_FAILED: i18n.t('common.errors.processingFailed'),
|
||||||
|
PDF_TEXT_EMPTY: i18n.t('common.errors.pdfTextEmpty'),
|
||||||
|
PDF_AI_INVALID_INPUT: i18n.t('common.errors.invalidInput'),
|
||||||
|
PDF_AI_ERROR: i18n.t('common.errors.processingFailed'),
|
||||||
|
PDF_TABLES_NOT_FOUND: i18n.t('common.errors.pdfNoTables'),
|
||||||
|
PDF_TABLE_EXTRACTION_FAILED: i18n.t('common.errors.processingFailed'),
|
||||||
|
TABULA_NOT_INSTALLED: i18n.t('common.errors.serverError'),
|
||||||
|
};
|
||||||
|
return map[errorCode] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getTaskErrorMessage(error: unknown, fallback: string): string {
|
export function getTaskErrorMessage(error: unknown, fallback: string): string {
|
||||||
|
if (isTaskErrorPayload(error)) {
|
||||||
|
// Prefer a translated message keyed by error_code
|
||||||
|
if (typeof error.error_code === 'string') {
|
||||||
|
const translated = resolveErrorCode(error.error_code);
|
||||||
|
if (translated) return translated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof error === 'string' && error.trim()) {
|
if (typeof error === 'string' && error.trim()) {
|
||||||
return error.trim();
|
return error.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user