feat: implement dark mode support and enhance UI components for better accessibility and insert new lang french.

This commit is contained in:
Your Name
2026-03-03 10:53:52 +02:00
parent 31f1e4b312
commit 071c66d3b1
13 changed files with 412 additions and 74 deletions

View File

@@ -32,7 +32,7 @@ const AddPageNumbers = lazy(() => import('@/components/tools/AddPageNumbers'));
function LoadingFallback() { function LoadingFallback() {
return ( return (
<div className="flex min-h-[40vh] items-center justify-center"> <div className="flex min-h-[40vh] items-center justify-center">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600" /> <div className="h-10 w-10 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600 dark:border-primary-800 dark:border-t-primary-400" />
</div> </div>
); );
} }
@@ -41,7 +41,7 @@ export default function App() {
useDirection(); useDirection();
return ( return (
<div className="flex min-h-screen flex-col bg-slate-50"> <div className="flex min-h-screen flex-col bg-slate-50 transition-colors duration-300 dark:bg-slate-950">
<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">

View File

@@ -6,11 +6,11 @@ export default function Footer() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<footer className="border-t border-slate-200 bg-slate-50"> <footer className="border-t border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-900">
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row"> <div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
{/* Brand */} {/* Brand */}
<div className="flex items-center gap-2 text-slate-600"> <div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
<FileText className="h-5 w-5" /> <FileText className="h-5 w-5" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
© {new Date().getFullYear()} {t('common.appName')} © {new Date().getFullYear()} {t('common.appName')}
@@ -21,19 +21,19 @@ export default function Footer() {
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<Link <Link
to="/privacy" to="/privacy"
className="text-sm text-slate-500 transition-colors hover:text-primary-600" className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
> >
{t('common.privacy')} {t('common.privacy')}
</Link> </Link>
<Link <Link
to="/terms" to="/terms"
className="text-sm text-slate-500 transition-colors hover:text-primary-600" className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
> >
{t('common.terms')} {t('common.terms')}
</Link> </Link>
<Link <Link
to="/about" to="/about"
className="text-sm text-slate-500 transition-colors hover:text-primary-600" className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
> >
{t('common.about')} {t('common.about')}
</Link> </Link>

View File

@@ -1,50 +1,174 @@
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FileText, Globe } from 'lucide-react'; import { FileText, Globe, Moon, Sun, Menu, X, ChevronDown } from 'lucide-react';
interface LangOption {
code: string;
label: string;
flag: string;
}
const languages: LangOption[] = [
{ code: 'en', label: 'English', flag: '🇺🇸' },
{ code: 'ar', label: 'العربية', flag: '🇸🇦' },
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
];
function useDarkMode() {
const [isDark, setIsDark] = useState(() => {
if (typeof window === 'undefined') return false;
const stored = localStorage.getItem('theme');
if (stored) return stored === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
const root = document.documentElement;
if (isDark) {
root.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
root.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [isDark]);
return { isDark, toggle: () => setIsDark((v) => !v) };
}
export default function Header() { export default function Header() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { isDark, toggle: toggleDark } = useDarkMode();
const [langOpen, setLangOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const langRef = useRef<HTMLDivElement>(null);
const toggleLanguage = () => { const currentLang = languages.find((l) => l.code === i18n.language) ?? languages[0];
const newLang = i18n.language === 'ar' ? 'en' : 'ar';
i18n.changeLanguage(newLang); // Close language dropdown on outside click
useEffect(() => {
function handleClick(e: MouseEvent) {
if (langRef.current && !langRef.current.contains(e.target as Node)) {
setLangOpen(false);
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
const switchLang = (code: string) => {
i18n.changeLanguage(code);
setLangOpen(false);
}; };
return ( return (
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg"> <header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg dark:border-slate-700 dark:bg-slate-900/80">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"> <div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */} {/* Logo */}
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600"> <Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600 dark:text-primary-400">
<FileText className="h-7 w-7" /> <FileText className="h-7 w-7" />
<span>{t('common.appName')}</span> <span>{t('common.appName')}</span>
</Link> </Link>
{/* Navigation */} {/* Desktop Navigation */}
<nav className="hidden items-center gap-6 md:flex"> <nav className="hidden items-center gap-6 md:flex">
<Link <Link
to="/" to="/"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600" className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
> >
{t('common.home')} {t('common.home')}
</Link> </Link>
<Link <Link
to="/about" to="/about"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600" className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
> >
{t('common.about')} {t('common.about')}
</Link> </Link>
</nav> </nav>
{/* Language Toggle */} {/* Actions */}
<button <div className="flex items-center gap-2">
onClick={toggleLanguage} {/* Dark Mode Toggle */}
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100" <button
aria-label={t('common.language')} onClick={toggleDark}
> className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
<Globe className="h-4 w-4" /> aria-label={isDark ? t('common.lightMode') : t('common.darkMode')}
<span>{i18n.language === 'ar' ? 'English' : 'العربية'}</span> title={isDark ? t('common.lightMode') : t('common.darkMode')}
</button> >
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
{/* Language Dropdown */}
<div className="relative" ref={langRef}>
<button
onClick={() => setLangOpen((v) => !v)}
className="flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
aria-label={t('common.language')}
aria-expanded={langOpen}
aria-haspopup="listbox"
>
<span className="text-lg leading-none">{currentLang.flag}</span>
<span className="hidden sm:inline">{currentLang.label}</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${langOpen ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown Menu */}
{langOpen && (
<div className="absolute end-0 top-full z-50 mt-2 w-44 origin-top-right animate-in fade-in slide-in-from-top-2 rounded-xl border border-slate-200 bg-white p-1 shadow-lg dark:border-slate-700 dark:bg-slate-800">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => switchLang(lang.code)}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
lang.code === i18n.language
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700'
}`}
role="option"
aria-selected={lang.code === i18n.language}
>
<span className="text-lg leading-none">{lang.flag}</span>
<span>{lang.label}</span>
{lang.code === i18n.language && (
<span className="ms-auto text-primary-600 dark:text-primary-400"></span>
)}
</button>
))}
</div>
)}
</div>
{/* Mobile Menu Toggle */}
<button
onClick={() => setMobileOpen((v) => !v)}
className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 md:hidden dark:text-slate-400 dark:hover:bg-slate-800"
aria-label="Menu"
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div> </div>
{/* Mobile Navigation */}
{mobileOpen && (
<nav className="border-t border-slate-200 bg-white px-4 pb-4 pt-2 md:hidden dark:border-slate-700 dark:bg-slate-900">
<Link
to="/"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.home')}
</Link>
<Link
to="/about"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.about')}
</Link>
</nav>
)}
</header> </header>
); );
} }

View File

@@ -16,13 +16,13 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
if (!result.download_url) return null; if (!result.download_url) return null;
return ( return (
<div className="rounded-2xl bg-emerald-50 p-6 ring-1 ring-emerald-200"> <div className="rounded-2xl bg-emerald-50 p-6 ring-1 ring-emerald-200 dark:bg-emerald-900/20 dark:ring-emerald-800">
{/* Success header */} {/* Success header */}
<div className="mb-4 text-center"> <div className="mb-4 text-center">
<p className="text-lg font-semibold text-emerald-800"> <p className="text-lg font-semibold text-emerald-800 dark:text-emerald-300">
{t('result.conversionComplete')} {t('result.conversionComplete')}
</p> </p>
<p className="mt-1 text-sm text-emerald-600"> <p className="mt-1 text-sm text-emerald-600 dark:text-emerald-400">
{t('result.downloadReady')} {t('result.downloadReady')}
</p> </p>
</div> </div>
@@ -31,24 +31,24 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
{(result.original_size || result.compressed_size) && ( {(result.original_size || result.compressed_size) && (
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3"> <div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
{result.original_size && ( {result.original_size && (
<div className="rounded-lg bg-white p-3 text-center"> <div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
<p className="text-xs text-slate-500">{t('result.originalSize')}</p> <p className="text-xs text-slate-500 dark:text-slate-400">{t('result.originalSize')}</p>
<p className="text-sm font-semibold text-slate-900"> <p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{formatFileSize(result.original_size)} {formatFileSize(result.original_size)}
</p> </p>
</div> </div>
)} )}
{result.compressed_size && ( {result.compressed_size && (
<div className="rounded-lg bg-white p-3 text-center"> <div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
<p className="text-xs text-slate-500">{t('result.newSize')}</p> <p className="text-xs text-slate-500 dark:text-slate-400">{t('result.newSize')}</p>
<p className="text-sm font-semibold text-slate-900"> <p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{formatFileSize(result.compressed_size)} {formatFileSize(result.compressed_size)}
</p> </p>
</div> </div>
)} )}
{result.reduction_percent !== undefined && ( {result.reduction_percent !== undefined && (
<div className="rounded-lg bg-white p-3 text-center"> <div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
<p className="text-xs text-slate-500">{t('result.reduction')}</p> <p className="text-xs text-slate-500 dark:text-slate-400">{t('result.reduction')}</p>
<p className="text-sm font-semibold text-emerald-600"> <p className="text-sm font-semibold text-emerald-600">
{result.reduction_percent}% {result.reduction_percent}%
</p> </p>
@@ -70,7 +70,7 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
</a> </a>
{/* Expiry notice */} {/* Expiry notice */}
<div className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500"> <div className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
<Clock className="h-3.5 w-3.5" /> <Clock className="h-3.5 w-3.5" />
{t('result.linkExpiry')} {t('result.linkExpiry')}
</div> </div>
@@ -78,7 +78,7 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
{/* Start over */} {/* Start over */}
<button <button
onClick={onStartOver} onClick={onStartOver}
className="mt-4 flex w-full items-center justify-center gap-2 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700" className="mt-4 flex w-full items-center justify-center gap-2 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
> >
<RotateCcw className="h-4 w-4" /> <RotateCcw className="h-4 w-4" />
{t('common.startOver')} {t('common.startOver')}

View File

@@ -69,13 +69,13 @@ export default function FileUploader({
isDragActive ? 'text-primary-500' : 'text-slate-400' isDragActive ? 'text-primary-500' : 'text-slate-400'
}`} }`}
/> />
<p className="mb-2 text-base font-medium text-slate-700"> <p className="mb-2 text-base font-medium text-slate-700 dark:text-slate-300">
{t('common.dragDrop')} {t('common.dragDrop')}
</p> </p>
{acceptLabel && ( {acceptLabel && (
<p className="text-sm text-slate-500">{acceptLabel}</p> <p className="text-sm text-slate-500 dark:text-slate-400">{acceptLabel}</p>
)} )}
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-slate-400 dark:text-slate-500">
{t('common.maxSize', { size: maxSizeMB })} {t('common.maxSize', { size: maxSizeMB })}
</p> </p>
</div> </div>
@@ -83,13 +83,13 @@ export default function FileUploader({
{/* Selected File */} {/* Selected File */}
{file && !isUploading && ( {file && !isUploading && (
<div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4 ring-1 ring-primary-200"> <div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4 ring-1 ring-primary-200 dark:bg-primary-900/20 dark:ring-primary-800">
<File className="h-8 w-8 flex-shrink-0 text-primary-600" /> <File className="h-8 w-8 flex-shrink-0 text-primary-600 dark:text-primary-400" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-slate-900"> <p className="truncate text-sm font-medium text-slate-900 dark:text-slate-100">
{file.name} {file.name}
</p> </p>
<p className="text-xs text-slate-500">{formatFileSize(file.size)}</p> <p className="text-xs text-slate-500 dark:text-slate-400">{formatFileSize(file.size)}</p>
</div> </div>
{onReset && ( {onReset && (
<button <button
@@ -105,12 +105,12 @@ export default function FileUploader({
{/* Upload Progress */} {/* Upload Progress */}
{isUploading && ( {isUploading && (
<div className="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200"> <div className="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-slate-700"> <span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.upload')}... {t('common.upload')}...
</span> </span>
<span className="text-sm text-slate-500">{uploadProgress}%</span> <span className="text-sm text-slate-500 dark:text-slate-400">{uploadProgress}%</span>
</div> </div>
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200"> <div className="h-2 w-full overflow-hidden rounded-full bg-slate-200">
<div <div
@@ -123,8 +123,8 @@ export default function FileUploader({
{/* Error */} {/* Error */}
{error && ( {error && (
<div className="mt-3 rounded-xl bg-red-50 p-3 ring-1 ring-red-200"> <div className="mt-3 rounded-xl bg-red-50 p-3 ring-1 ring-red-200 dark:bg-red-900/20 dark:ring-red-800">
<p className="text-sm text-red-700">{error}</p> <p className="text-sm text-red-700 dark:text-red-400">{error}</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -15,17 +15,17 @@ export default function ProgressBar({ state, message }: ProgressBarProps) {
const isComplete = state === 'SUCCESS'; const isComplete = state === 'SUCCESS';
return ( return (
<div className="rounded-xl bg-slate-50 p-5 ring-1 ring-slate-200"> <div className="rounded-xl bg-slate-50 p-5 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isActive && ( {isActive && (
<Loader2 className="h-6 w-6 animate-spin text-primary-600" /> <Loader2 className="h-6 w-6 animate-spin text-primary-600 dark:text-primary-400" />
)} )}
{isComplete && ( {isComplete && (
<CheckCircle2 className="h-6 w-6 text-emerald-600" /> <CheckCircle2 className="h-6 w-6 text-emerald-600" />
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-slate-700"> <p className="text-sm font-medium text-slate-700 dark:text-slate-300">
{message || t('common.processing')} {message || t('common.processing')}
</p> </p>
</div> </div>
@@ -33,7 +33,7 @@ export default function ProgressBar({ state, message }: ProgressBarProps) {
{/* Animated progress bar for active states */} {/* Animated progress bar for active states */}
{isActive && ( {isActive && (
<div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200"> <div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div className="progress-bar-animated h-full w-2/3 rounded-full bg-primary-500 transition-all" /> <div className="progress-bar-animated h-full w-2/3 rounded-full bg-primary-500 transition-all" />
</div> </div>
)} )}

View File

@@ -30,10 +30,10 @@ export default function ToolCard({
{icon} {icon}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-base font-semibold text-slate-900 group-hover:text-primary-600 transition-colors"> <h3 className="text-base font-semibold text-slate-900 group-hover:text-primary-600 transition-colors dark:text-slate-100 dark:group-hover:text-primary-400">
{title} {title}
</h3> </h3>
<p className="mt-1 text-sm text-slate-500 line-clamp-2"> <p className="mt-1 text-sm text-slate-500 line-clamp-2 dark:text-slate-400">
{description} {description}
</p> </p>
</div> </div>

View File

@@ -17,7 +17,9 @@
"privacy": "سياسة الخصوصية", "privacy": "سياسة الخصوصية",
"terms": "شروط الاستخدام", "terms": "شروط الاستخدام",
"language": "اللغة", "language": "اللغة",
"allTools": "كل الأدوات" "allTools": "كل الأدوات",
"darkMode": "الوضع الداكن",
"lightMode": "الوضع الفاتح"
}, },
"home": { "home": {
"hero": "حوّل ملفاتك فوراً", "hero": "حوّل ملفاتك فوراً",

View File

@@ -17,7 +17,9 @@
"privacy": "Privacy Policy", "privacy": "Privacy Policy",
"terms": "Terms of Service", "terms": "Terms of Service",
"language": "Language", "language": "Language",
"allTools": "All Tools" "allTools": "All Tools",
"darkMode": "Dark Mode",
"lightMode": "Light Mode"
}, },
"home": { "home": {
"hero": "Transform Your Files Instantly", "hero": "Transform Your Files Instantly",

184
frontend/src/i18n/fr.json Normal file
View File

@@ -0,0 +1,184 @@
{
"common": {
"appName": "SaaS-PDF",
"tagline": "Outils de fichiers en ligne gratuits",
"upload": "Télécharger un fichier",
"download": "Télécharger",
"processing": "Traitement en cours...",
"dragDrop": "Glissez-déposez votre fichier ici, ou cliquez pour parcourir",
"maxSize": "Taille maximale du fichier : {{size}} Mo",
"tryOtherTools": "Essayer d'autres outils",
"error": "Erreur",
"success": "Succès",
"loading": "Chargement...",
"startOver": "Recommencer",
"home": "Accueil",
"about": "À propos",
"privacy": "Politique de confidentialité",
"terms": "Conditions d'utilisation",
"language": "Langue",
"allTools": "Tous les outils",
"darkMode": "Mode sombre",
"lightMode": "Mode clair"
},
"home": {
"hero": "Transformez vos fichiers instantanément",
"heroSub": "Outils en ligne gratuits pour le traitement de PDF, images, vidéos et textes. Aucune inscription requise.",
"popularTools": "Outils populaires",
"pdfTools": "Outils PDF",
"imageTools": "Outils d'images",
"videoTools": "Outils vidéo",
"textTools": "Outils de texte"
},
"tools": {
"pdfToWord": {
"title": "PDF vers Word",
"description": "Convertissez gratuitement des fichiers PDF en documents Word modifiables.",
"shortDesc": "PDF → Word"
},
"wordToPdf": {
"title": "Word vers PDF",
"description": "Convertissez gratuitement des documents Word (DOC, DOCX) au format PDF.",
"shortDesc": "Word → PDF"
},
"compressPdf": {
"title": "Compresser PDF",
"description": "Réduisez la taille du fichier PDF tout en maintenant la qualité. Choisissez votre niveau de compression.",
"shortDesc": "Compresser PDF",
"qualityLow": "Compression maximale",
"qualityMedium": "Équilibré",
"qualityHigh": "Haute qualité"
},
"imageConvert": {
"title": "Convertisseur d'images",
"description": "Convertissez instantanément des images entre les formats JPG, PNG et WebP.",
"shortDesc": "Convertir des images"
},
"videoToGif": {
"title": "Vidéo en GIF",
"description": "Créez des GIFs animés à partir de clips vidéo. Personnalisez le temps de début, la durée et la qualité.",
"shortDesc": "Vidéo → GIF",
"startTime": "Temps de début (secondes)",
"duration": "Durée (secondes)",
"fps": "Images par seconde",
"width": "Largeur (pixels)"
},
"wordCounter": {
"title": "Compteur de mots",
"description": "Comptez instantanément les mots, caractères, phrases et paragraphes de votre texte.",
"shortDesc": "Compter les mots",
"words": "Mots",
"characters": "Caractères",
"sentences": "Phrases",
"paragraphs": "Paragraphes",
"placeholder": "Tapez ou collez votre texte ici..."
},
"textCleaner": {
"title": "Nettoyeur de texte",
"description": "Supprimez les espaces en trop, convertissez la casse et nettoyez votre texte instantanément.",
"shortDesc": "Nettoyer le texte",
"removeSpaces": "Supprimer les espaces en trop",
"toUpperCase": "MAJUSCULES",
"toLowerCase": "minuscules",
"toTitleCase": "Casse Du Titre",
"toSentenceCase": "Casse de phrase",
"removeDiacritics": "Supprimer les signes diacritiques arabes",
"copyResult": "Copier le résultat"
},
"mergePdf": {
"title": "Fusionner PDF",
"description": "Combinez plusieurs fichiers PDF en un seul document. Gratuit et rapide.",
"shortDesc": "Fusionner PDF",
"selectFiles": "Sélectionner des fichiers PDF",
"addMore": "Ajouter plus de fichiers",
"filesSelected": "{{count}} fichiers sélectionnés",
"dragToReorder": "Glissez les fichiers pour les réorganiser"
},
"splitPdf": {
"title": "Diviser PDF",
"description": "Divisez un PDF en pages individuelles ou extrayez des plages de pages spécifiques.",
"shortDesc": "Diviser PDF",
"allPages": "Toutes les pages",
"pageRange": "Plage de pages",
"rangeHint": "ex. 1,3,5-8",
"rangePlaceholder": "Entrez les pages : 1,3,5-8"
},
"rotatePdf": {
"title": "Pivoter PDF",
"description": "Pivotez toutes les pages d'un PDF de 90°, 180° ou 270° degrés.",
"shortDesc": "Pivoter PDF",
"angle": "Angle de rotation",
"90": "90° Sens horaire",
"180": "180° Retourner",
"270": "270° Sens anti-horaire"
},
"pdfToImages": {
"title": "PDF en images",
"description": "Convertissez chaque page d'un PDF en images haute qualité (PNG ou JPG).",
"shortDesc": "PDF → Images",
"format": "Format d'image",
"dpi": "Résolution (DPI)",
"dpiLow": "72 — Écran",
"dpiMedium": "150 — Standard",
"dpiHigh": "200 — Bon",
"dpiUltra": "300 — Qualité d'impression"
},
"imagesToPdf": {
"title": "Images en PDF",
"description": "Combinez plusieurs images en un seul document PDF.",
"shortDesc": "Images → PDF",
"selectImages": "Sélectionner des images",
"addMore": "Ajouter plus d'images",
"imagesSelected": "{{count}} images sélectionnées"
},
"watermarkPdf": {
"title": "Filigrane PDF",
"description": "Ajoutez un filigrane de texte personnalisé à chaque page de votre PDF.",
"shortDesc": "Ajouter un filigrane",
"text": "Texte du filigrane",
"textPlaceholder": "Entrez le texte du filigrane",
"opacity": "Opacité",
"light": "Léger",
"heavy": "Dense"
},
"protectPdf": {
"title": "Protéger PDF",
"description": "Ajoutez une protection par mot de passe à votre PDF pour empêcher tout accès non autorisé.",
"shortDesc": "Protéger PDF",
"password": "Mot de passe",
"passwordPlaceholder": "Entrez un mot de passe fort",
"confirmPassword": "Confirmer le mot de passe",
"confirmPlaceholder": "Ressaisissez le mot de passe",
"mismatch": "Les mots de passe ne correspondent pas"
},
"unlockPdf": {
"title": "Déverrouiller PDF",
"description": "Supprimez la protection par mot de passe de votre fichier PDF.",
"shortDesc": "Déverrouiller PDF",
"password": "Mot de passe actuel",
"passwordPlaceholder": "Entrez le mot de passe du PDF"
},
"pageNumbers": {
"title": "Ajouter des numéros de page",
"description": "Ajoutez des numéros de page à chaque page de votre PDF. Choisissez la position et le numéro de début.",
"shortDesc": "Numéros de page",
"position": "Position du numéro",
"startNumber": "Commencer à partir de",
"bottomCenter": "Bas centre",
"bottomRight": "Bas droite",
"bottomLeft": "Bas gauche",
"topCenter": "Haut centre",
"topRight": "Haut droite",
"topLeft": "Haut gauche"
}
},
"result": {
"conversionComplete": "Conversion terminée !",
"compressionComplete": "Compression terminée !",
"originalSize": "Taille originale",
"newSize": "Nouvelle taille",
"reduction": "Réduction",
"downloadReady": "Votre fichier est prêt à être téléchargé.",
"linkExpiry": "Le lien de téléchargement expire dans 30 minutes."
}
}

View File

@@ -4,6 +4,7 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import en from './en.json'; import en from './en.json';
import ar from './ar.json'; import ar from './ar.json';
import fr from './fr.json';
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)
@@ -12,9 +13,10 @@ i18n
resources: { resources: {
en: { translation: en }, en: { translation: en },
ar: { translation: ar }, ar: { translation: ar },
fr: { translation: fr },
}, },
fallbackLng: 'en', fallbackLng: 'en',
supportedLngs: ['en', 'ar'], supportedLngs: ['en', 'ar', 'fr'],
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },

View File

@@ -74,10 +74,10 @@ export default function HomePage() {
{/* Hero Section */} {/* Hero Section */}
<section className="py-12 text-center sm:py-16"> <section className="py-12 text-center sm:py-16">
<h1 className="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl"> <h1 className="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl dark:text-white">
{t('home.hero')} {t('home.hero')}
</h1> </h1>
<p className="mx-auto mt-4 max-w-xl text-lg text-slate-500"> <p className="mx-auto mt-4 max-w-xl text-lg text-slate-500 dark:text-slate-400">
{t('home.heroSub')} {t('home.heroSub')}
</p> </p>
</section> </section>
@@ -87,7 +87,7 @@ export default function HomePage() {
{/* Tools Grid */} {/* Tools Grid */}
<section> <section>
<h2 className="mb-6 text-center text-xl font-semibold text-slate-800"> <h2 className="mb-6 text-center text-xl font-semibold text-slate-800 dark:text-slate-200">
{t('home.popularTools')} {t('home.popularTools')}
</h2> </h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">

View File

@@ -11,12 +11,20 @@
--color-border: #e2e8f0; --color-border: #e2e8f0;
} }
.dark {
--color-bg: #0f172a;
--color-surface: #1e293b;
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #334155;
}
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
body { body {
@apply bg-white text-slate-900 antialiased; @apply bg-white text-slate-900 antialiased dark:bg-slate-950 dark:text-slate-100;
font-family: 'Inter', 'Tajawal', system-ui, sans-serif; font-family: 'Inter', 'Tajawal', system-ui, sans-serif;
} }
@@ -32,46 +40,46 @@
@layer components { @layer components {
.btn-primary { .btn-primary {
@apply inline-flex items-center justify-center gap-2 rounded-xl bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:opacity-50 disabled:cursor-not-allowed; @apply inline-flex items-center justify-center gap-2 rounded-xl bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-primary-500 dark:hover:bg-primary-600;
} }
.btn-secondary { .btn-secondary {
@apply inline-flex items-center justify-center gap-2 rounded-xl bg-white px-6 py-3 text-sm font-semibold text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 transition-all hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed; @apply inline-flex items-center justify-center gap-2 rounded-xl bg-white px-6 py-3 text-sm font-semibold text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 transition-all hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-slate-800 dark:text-slate-100 dark:ring-slate-600 dark:hover:bg-slate-700;
} }
.btn-success { .btn-success {
@apply inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed; @apply inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-emerald-500 dark:hover:bg-emerald-600;
} }
.card { .card {
@apply rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200 transition-shadow hover:shadow-md; @apply rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200 transition-shadow hover:shadow-md dark:bg-slate-800 dark:ring-slate-700;
} }
.tool-card { .tool-card {
@apply card cursor-pointer hover:ring-primary-300 hover:shadow-lg transition-all duration-200; @apply card cursor-pointer hover:ring-primary-300 hover:shadow-lg transition-all duration-200 dark:hover:ring-primary-600;
} }
.input-field { .input-field {
@apply block w-full rounded-xl border-0 py-3 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6; @apply block w-full rounded-xl border-0 py-3 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 dark:bg-slate-800 dark:text-slate-100 dark:ring-slate-600 dark:placeholder:text-slate-500;
} }
.section-heading { .section-heading {
@apply text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl; @apply text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-100;
} }
} }
/* Upload zone styles */ /* Upload zone styles */
.upload-zone { .upload-zone {
@apply flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-slate-300 bg-slate-50 p-8 text-center transition-colors cursor-pointer; @apply flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-slate-300 bg-slate-50 p-8 text-center transition-colors cursor-pointer dark:border-slate-600 dark:bg-slate-800/50;
} }
.upload-zone:hover, .upload-zone:hover,
.upload-zone.drag-active { .upload-zone.drag-active {
@apply border-primary-400 bg-primary-50; @apply border-primary-400 bg-primary-50 dark:border-primary-500 dark:bg-primary-900/20;
} }
.upload-zone.drag-active { .upload-zone.drag-active {
@apply ring-2 ring-primary-300; @apply ring-2 ring-primary-300 dark:ring-primary-600;
} }
/* Progress bar animation */ /* Progress bar animation */
@@ -86,5 +94,21 @@
/* Ad slot container */ /* Ad slot container */
.ad-slot { .ad-slot {
@apply flex items-center justify-center bg-slate-50 rounded-xl border border-slate-200 min-h-[90px] overflow-hidden; @apply flex items-center justify-center bg-slate-50 rounded-xl border border-slate-200 min-h-[90px] overflow-hidden dark:bg-slate-800 dark:border-slate-700;
}
/* Dropdown animation */
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeSlideIn 0.15s ease-out;
} }