From e5901f662f1d0456e0d712a150af05dd753a1ac6 Mon Sep 17 00:00:00 2001 From: Kouros Zanbouri Date: Fri, 31 Jul 2026 12:51:54 +0100 Subject: [PATCH 01/10] Add Farsi (Persian) translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete fa locale with 1146 translated keys (266 common, 880 tools). Register 'fa' in supportedLanguages, languageNames, and SUPPORTED_LANGUAGES. Also add 'fa' to the three hardcoded language regexes in i18n.ts (getLanguageFromUrl, changeLanguage, rewriteLinks) — these are not mentioned in TRANSLATION.md, but without them /fa/ silently falls back to English. Persian is RTL, so extend the dir=rtl check from 'ar' to ['ar','fa'] in both i18n.ts and generate-i18n-pages.mjs. --- public/locales/fa/common.json | 387 +++++++++++ public/locales/fa/tools.json | 1154 +++++++++++++++++++++++++++++++ scripts/generate-i18n-pages.mjs | 2 +- src/js/i18n/i18n.ts | 12 +- vite.config.ts | 1 + 5 files changed, 1551 insertions(+), 5 deletions(-) create mode 100644 public/locales/fa/common.json create mode 100644 public/locales/fa/tools.json diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json new file mode 100644 index 00000000..e0038eea --- /dev/null +++ b/public/locales/fa/common.json @@ -0,0 +1,387 @@ +{ + "nav": { + "home": "خانه", + "about": "درباره ما", + "contact": "تماس با ما", + "licensing": "مجوزها", + "allTools": "همه ابزارها", + "docs": "مستندات", + "openMainMenu": "باز کردن منوی اصلی", + "language": "زبان", + "searchLanguage": "جستجوی زبان…", + "noLanguagesFound": "زبانی یافت نشد" + }, + "donation": { + "message": "BentoPDF را دوست دارید؟ کمک کنید رایگان و متن‌باز بماند!", + "button": "حمایت مالی" + }, + "hero": { + "title": "مجموعهٔ", + "pdfToolkit": "ابزارهای PDF", + "builtForPrivacy": "ساخته‌شده برای حریم خصوصی", + "noSignups": "بدون ثبت‌نام", + "unlimitedUse": "استفادهٔ نامحدود", + "worksOffline": "کار بدون اینترنت", + "startUsing": "همین حالا شروع کنید" + }, + "usedBy": { + "title": "مورد استفادهٔ شرکت‌ها و افرادی که در این مجموعه‌ها کار می‌کنند" + }, + "features": { + "title": "چرا", + "bentoPdf": "BentoPDF؟", + "noSignup": { + "title": "بدون ثبت‌نام", + "description": "بی‌درنگ شروع کنید؛ بدون حساب کاربری یا ایمیل." + }, + "noUploads": { + "title": "بدون بارگذاری", + "description": "۱۰۰٪ سمت کاربر؛ فایل‌های شما هرگز از دستگاهتان خارج نمی‌شوند." + }, + "foreverFree": { + "title": "همیشه رایگان", + "description": "همهٔ ابزارها، بدون دورهٔ آزمایشی و بدون پرداخت." + }, + "noLimits": { + "title": "بدون محدودیت", + "description": "هر قدر که می‌خواهید استفاده کنید؛ بدون سقف پنهان." + }, + "batchProcessing": { + "title": "پردازش گروهی", + "description": "تعداد نامحدودی PDF را یکجا پردازش کنید." + }, + "lightningFast": { + "title": "برق‌آسا", + "description": "PDFها را بی‌درنگ پردازش کنید؛ بدون انتظار و تأخیر." + } + }, + "tools": { + "title": "شروع کنید با", + "toolsLabel": "ابزارها", + "subtitle": "برای باز کردن بارگذار فایل، روی یک ابزار کلیک کنید", + "searchPlaceholder": "جستجوی ابزار (مثلاً «تقسیم»، «سازماندهی»…)", + "backToTools": "بازگشت به ابزارها", + "firstLoadNotice": "بارگذاری نخست کمی طول می‌کشد چون موتور تبدیل دانلود می‌شود. پس از آن، همهٔ بارگذاری‌ها آنی خواهند بود." + }, + "upload": { + "clickToSelect": "برای انتخاب فایل کلیک کنید", + "orDragAndDrop": "یا فایل را بکشید و رها کنید", + "pdfOrImages": "PDF یا تصویر", + "filesNeverLeave": "فایل‌های شما هرگز از دستگاهتان خارج نمی‌شوند.", + "addMore": "افزودن فایل بیشتر", + "clearAll": "پاک کردن همه", + "clearFiles": "پاک کردن فایل‌ها", + "hints": { + "singlePdf": "یک فایل PDF", + "pdfFile": "فایل PDF", + "multiplePdfs2": "چند فایل PDF (حداقل ۲ فایل)", + "bmpImages": "تصاویر BMP", + "oneOrMorePdfs": "یک یا چند فایل PDF", + "pdfDocuments": "اسناد PDF", + "oneOrMoreCsv": "یک یا چند فایل CSV", + "multiplePdfsSupported": "چند فایل PDF پشتیبانی می‌شود", + "singleOrMultiplePdfs": "یک یا چند فایل PDF پشتیبانی می‌شود", + "singlePdfFile": "یک فایل PDF", + "pdfWithForms": "فایل PDF دارای فیلدهای فرم", + "heicImages": "تصاویر HEIC/HEIF", + "jpgImages": "تصاویر JPG، JPEG، JP2، JPX", + "pdfsOrImages": "PDF یا تصویر", + "oneOrMoreOdt": "یک یا چند فایل ODT", + "singlePdfOnly": "فقط یک فایل PDF", + "pdfFiles": "فایل‌های PDF", + "multiplePdfs": "چند فایل PDF", + "pngImages": "تصاویر PNG", + "pdfFilesOneOrMore": "فایل‌های PDF (یک یا چند)", + "oneOrMoreRtf": "یک یا چند فایل RTF", + "svgGraphics": "گرافیک‌های SVG", + "tiffImages": "تصاویر TIFF", + "webpImages": "تصاویر WebP" + } + }, + "howItWorks": { + "title": "چگونه کار می‌کند", + "step1Title": "بارگذاری فایل", + "step1": "برای شروع، فایل خود را کلیک کنید یا بکشید و رها کنید", + "step2Title": "پردازش", + "step2": "برای شروع، روی دکمهٔ پردازش کلیک کنید", + "step3Title": "دانلود", + "step3": "فایل پردازش‌شدهٔ خود را بی‌درنگ ذخیره کنید" + }, + "relatedTools": { + "title": "ابزارهای مرتبط PDF" + }, + "loader": { + "processing": "در حال پردازش…" + }, + "alert": { + "title": "هشدار", + "ok": "باشه" + }, + "preview": { + "title": "پیش‌نمایش سند", + "downloadAsPdf": "دانلود به‌صورت PDF", + "close": "بستن" + }, + "settings": { + "title": "تنظیمات", + "shortcuts": "میان‌برها", + "preferences": "ترجیحات", + "displayPreferences": "ترجیحات نمایش", + "searchShortcuts": "جستجوی میان‌برها…", + "shortcutsInfo": "برای تعیین میان‌بر، کلیدها را فشار داده و نگه دارید. تغییرات به‌طور خودکار ذخیره می‌شوند.", + "shortcutsWarning": "⚠️ از میان‌برهای رایج مرورگر (Cmd/Ctrl+W، Cmd/Ctrl+T، Cmd/Ctrl+N و غیره) پرهیز کنید، زیرا ممکن است به‌درستی کار نکنند.", + "import": "درون‌ریزی", + "export": "برون‌ریزی", + "resetToDefaults": "بازنشانی به پیش‌فرض‌ها", + "fullWidthMode": "حالت تمام‌عرض", + "fullWidthDescription": "به‌جای کادر وسط‌چین، از تمام عرض صفحه برای همهٔ ابزارها استفاده کنید", + "compactMode": "حالت فشرده", + "compactModeDescription": "نمایش ابزارها به‌صورت فهرست فشرده به‌جای کارت", + "advancedSettings": "تنظیمات پیشرفته", + "advancedSettingsDescription": "پیکربندی ماژول‌های پردازش خارجی (PyMuPDF، Ghostscript، CoherentPDF)", + "settingsAutoSaved": "تنظیمات به‌طور خودکار ذخیره می‌شوند", + "clickToSet": "برای تعیین کلیک کنید", + "pressKeys": "کلیدها را فشار دهید…", + "warnings": { + "alreadyInUse": "این میان‌بر از پیش استفاده شده است", + "assignedTo": "پیش‌تر به این مورد اختصاص یافته است:", + "chooseDifferent": "لطفاً میان‌بر دیگری انتخاب کنید.", + "reserved": "هشدار میان‌بر رزرو‌شده", + "commonlyUsed": "معمولاً برای این مورد استفاده می‌شود:", + "unreliable": "ممکن است این میان‌بر به‌درستی کار نکند یا با رفتار مرورگر/سیستم تداخل داشته باشد.", + "useAnyway": "آیا می‌خواهید با این حال از آن استفاده کنید؟", + "resetTitle": "بازنشانی میان‌برها", + "resetMessage": "آیا مطمئنید می‌خواهید همهٔ میان‌برها را به حالت پیش‌فرض بازنشانی کنید؟

این عمل قابل بازگشت نیست.", + "importSuccessTitle": "درون‌ریزی موفق", + "importSuccessMessage": "میان‌برها با موفقیت درون‌ریزی شدند!", + "importFailTitle": "درون‌ریزی ناموفق", + "importFailMessage": "درون‌ریزی میان‌برها ناموفق بود. قالب فایل نامعتبر است." + } + }, + "warning": { + "title": "هشدار", + "cancel": "انصراف", + "proceed": "ادامه" + }, + "compliance": { + "title": "داده‌های شما هرگز از دستگاهتان خارج نمی‌شوند", + "weKeep": "ما با رعایت استانداردهای جهانی امنیت،", + "yourInfoSafe": "اطلاعات شما را ایمن", + "byFollowingStandards": "نگه می‌داریم.", + "processingLocal": "تمام پردازش‌ها به‌صورت محلی روی دستگاه شما انجام می‌شود.", + "gdpr": { + "title": "انطباق با GDPR", + "description": "از داده‌های شخصی و حریم خصوصی افراد در اتحادیهٔ اروپا محافظت می‌کند." + }, + "ccpa": { + "title": "انطباق با CCPA", + "description": "به ساکنان کالیفرنیا حق تعیین نحوهٔ گردآوری، استفاده و اشتراک‌گذاری اطلاعات شخصی‌شان را می‌دهد." + }, + "hipaa": { + "title": "انطباق با HIPAA", + "description": "تدابیر حفاظتی برای مدیریت اطلاعات حساس سلامت در نظام بهداشت و درمان ایالات متحده تعیین می‌کند." + } + }, + "faq": { + "title": "سؤالات", + "questions": "متداول", + "sectionTitle": "سؤالات متداول", + "isFree": { + "question": "آیا BentoPDF واقعاً رایگان است؟", + "answer": "بله، کاملاً. همهٔ ابزارهای BentoPDF ۱۰۰٪ رایگان هستند؛ بدون محدودیت فایل، بدون ثبت‌نام و بدون واترمارک. ما باور داریم همه سزاوار دسترسی به ابزارهای ساده و قدرتمند PDF بدون پرداخت هزینه هستند." + }, + "areFilesSecure": { + "question": "آیا فایل‌های من امن هستند؟ کجا پردازش می‌شوند؟", + "answer": "فایل‌های شما تا حد ممکن امن هستند، زیرا هرگز از رایانهٔ شما خارج نمی‌شوند. تمام پردازش‌ها مستقیماً در مرورگر شما (سمت کاربر) انجام می‌شود. ما هرگز فایل‌های شما را روی سرور بارگذاری نمی‌کنیم، بنابراین حریم خصوصی و کنترل کامل اسنادتان نزد خودتان می‌ماند." + }, + "platforms": { + "question": "آیا روی مک، ویندوز و موبایل کار می‌کند؟", + "answer": "بله! از آنجا که BentoPDF تماماً در مرورگر شما اجرا می‌شود، روی هر سیستم‌عاملی با یک مرورگر مدرن کار می‌کند؛ از جمله ویندوز، macOS، لینوکس، iOS و اندروید." + }, + "gdprCompliant": { + "question": "آیا BentoPDF با GDPR سازگار است؟", + "answer": "بله. BentoPDF کاملاً با GDPR سازگار است. از آنجا که تمام پردازش فایل‌ها به‌صورت محلی در مرورگر شما انجام می‌شود و ما هرگز فایل‌های شما را گردآوری یا به سروری ارسال نمی‌کنیم، هیچ دسترسی‌ای به داده‌های شما نداریم. این تضمین می‌کند که همیشه کنترل اسنادتان را در دست دارید." + }, + "dataStorage": { + "question": "آیا فایل‌های مرا ذخیره یا ردیابی می‌کنید؟", + "answer": "خیر. ما هرگز فایل‌های شما را ذخیره، ردیابی یا ثبت نمی‌کنیم. هر کاری در BentoPDF انجام می‌دهید در حافظهٔ مرورگر شما رخ می‌دهد و با بستن صفحه از بین می‌رود. هیچ بارگذاری، هیچ گزارش تاریخچه و هیچ سروری در کار نیست." + }, + "different": { + "question": "چه چیزی BentoPDF را از دیگر ابزارهای PDF متمایز می‌کند؟", + "answer": "بیشتر ابزارهای PDF فایل‌های شما را برای پردازش روی سرور بارگذاری می‌کنند. BentoPDF هرگز این کار را نمی‌کند. ما از فناوری وب امن و مدرن برای پردازش فایل‌ها مستقیماً در مرورگر شما استفاده می‌کنیم. این یعنی کارایی سریع‌تر، حریم خصوصی قوی‌تر و آسودگی خاطر کامل." + }, + "browserBased": { + "question": "پردازش مبتنی بر مرورگر چگونه امنیت مرا حفظ می‌کند؟", + "answer": "با اجرای کامل در درون مرورگر شما، BentoPDF تضمین می‌کند که فایل‌هایتان هرگز از دستگاهتان خارج نشوند. این کار خطر هک شدن سرور، نشت داده یا دسترسی غیرمجاز را از بین می‌برد. فایل‌های شما همیشه مال خودتان می‌مانند." + }, + "analytics": { + "question": "آیا از کوکی یا ابزارهای تحلیلی برای ردیابی من استفاده می‌کنید؟", + "answer": "حریم خصوصی شما برای ما مهم است. BentoPDF اطلاعات شخصی را ردیابی نمی‌کند. ما تنها از Simple Analytics برای مشاهدهٔ شمار بازدیدهای ناشناس استفاده می‌کنیم. یعنی می‌دانیم چند کاربر از سایت ما بازدید کرده‌اند، اما هرگز نمی‌دانیم شما چه کسی هستید. Simple Analytics کاملاً با GDPR سازگار است و به حریم خصوصی شما احترام می‌گذارد." + } + }, + "testimonials": { + "title": "نظر", + "users": "کاربران", + "say": "ما چیست" + }, + "support": { + "title": "کارم را می‌پسندید؟", + "description": "BentoPDF یک پروژهٔ عاشقانه است که برای ارائهٔ مجموعه‌ابزار PDF رایگان، خصوصی و قدرتمند به همه ساخته شده است. اگر برایتان مفید بود، از توسعهٔ آن حمایت کنید. هر قهوه کمک می‌کند!", + "buyMeCoffee": "برایم یک قهوه بخرید" + }, + "footer": { + "copyright": "© ۲۰۲۶ BentoPDF. تمامی حقوق محفوظ است.", + "version": "نسخه", + "company": "شرکت", + "aboutUs": "دربارهٔ ما", + "faqLink": "سؤالات متداول", + "contactUs": "تماس با ما", + "legal": "حقوقی", + "termsAndConditions": "شرایط و ضوابط", + "privacyPolicy": "سیاست حفظ حریم خصوصی", + "followUs": "ما را دنبال کنید" + }, + "merge": { + "title": "ادغام PDFها", + "description": "فایل‌ها را به‌طور کامل ترکیب کنید، یا صفحات مشخصی را برای ادغام در یک سند تازه برگزینید.", + "fileMode": "حالت فایل", + "pageMode": "حالت صفحه", + "howItWorks": "چگونه کار می‌کند:", + "fileModeInstructions": [ + "برای تغییر ترتیب فایل‌ها، نماد را کلیک کرده و بکشید.", + "در کادر «صفحات» هر فایل می‌توانید بازه‌ها را مشخص کنید (مثلاً «۱-۳، ۵») تا فقط همان صفحات ادغام شوند.", + "برای گنجاندن همهٔ صفحات آن فایل، کادر «صفحات» را خالی بگذارید." + ], + "pageModeInstructions": [ + "همهٔ صفحات PDFهای بارگذاری‌شدهٔ شما در زیر نمایش داده می‌شوند.", + "کافی است بندانگشتی صفحات را بکشید و رها کنید تا ترتیب دلخواه فایل تازه‌تان ساخته شود." + ], + "mergePdfs": "ادغام PDFها" + }, + "common": { + "page": "صفحه", + "pages": "صفحات", + "of": "از", + "download": "دانلود", + "cancel": "انصراف", + "save": "ذخیره", + "delete": "حذف", + "edit": "ویرایش", + "add": "افزودن", + "remove": "برداشتن", + "loading": "در حال بارگذاری…", + "loadingPageCount": "در حال بارگذاری صفحات…", + "error": "خطا", + "success": "موفق", + "file": "فایل", + "files": "فایل‌ها", + "close": "بستن", + "convert": "تبدیل", + "unknownError": "خطای ناشناخته" + }, + "about": { + "hero": { + "title": "ما باور داریم ابزارهای PDF باید", + "subtitle": "سریع، خصوصی و رایگان باشند.", + "noCompromises": "بدون هیچ مصالحه‌ای." + }, + "mission": { + "title": "مأموریت ما", + "description": "ارائهٔ جامع‌ترین جعبه‌ابزار PDF که به حریم خصوصی شما احترام می‌گذارد و هرگز درخواست پرداخت نمی‌کند. ما باور داریم ابزارهای ضروری اسناد باید برای همه، در همه جا و بدون مانع در دسترس باشند." + }, + "philosophy": { + "label": "فلسفهٔ بنیادین ما", + "title": "نخست حریم خصوصی. همیشه.", + "description": "در روزگاری که داده به کالا تبدیل شده است، ما راه دیگری در پیش گرفته‌ایم. تمام پردازش ابزارهای Bentopdf به‌صورت محلی در مرورگر شما انجام می‌شود. این یعنی فایل‌های شما هرگز به سرورهای ما نمی‌رسند، ما هرگز اسنادتان را نمی‌بینیم و کارهایتان را ردیابی نمی‌کنیم. اسناد شما به‌طور کامل و بی‌چون‌وچرا خصوصی می‌مانند. این تنها یک قابلیت نیست؛ بنیان کار ماست." + }, + "whyBentopdf": { + "title": "چرا", + "speed": { + "title": "ساخته‌شده برای سرعت", + "description": "دیگر نیازی به انتظار برای بارگذاری یا دانلود از سرور نیست. با پردازش فایل‌ها مستقیماً در مرورگر شما و با فناوری‌های وب مدرن مانند WebAssembly، سرعتی بی‌همتا در همهٔ ابزارهایمان ارائه می‌دهیم." + }, + "free": { + "title": "کاملاً رایگان", + "description": "بدون دورهٔ آزمایشی، بدون اشتراک، بدون هزینهٔ پنهان و بدون قابلیت‌های «ویژه»‌ای که در گرو پرداخت باشند. ما باور داریم ابزارهای قدرتمند PDF باید یک خدمت همگانی باشند، نه منبع سود." + }, + "noAccount": { + "title": "بدون نیاز به حساب کاربری", + "description": "بی‌درنگ از هر ابزاری استفاده کنید. ما به ایمیل، گذرواژه یا هیچ اطلاعات شخصی شما نیاز نداریم. روند کار شما باید بی‌دردسر و ناشناس باشد." + }, + "openSource": { + "title": "روحیهٔ متن‌باز", + "description": "با نگاهی به شفافیت ساخته شده است. ما از کتابخانه‌های متن‌باز فوق‌العاده‌ای مانند PDF-lib و PDF.js بهره می‌گیریم و به تلاش جمعی جامعه برای در دسترس قرار دادن ابزارهای قدرتمند برای همه باور داریم." + } + }, + "cta": { + "title": "آمادهٔ شروع هستید؟", + "description": "به هزاران کاربری بپیوندید که برای نیازهای روزمرهٔ اسنادشان به Bentopdf اعتماد می‌کنند. تفاوتی را که حریم خصوصی و کارایی رقم می‌زند، تجربه کنید.", + "button": "کاوش در همهٔ ابزارها" + } + }, + "contact": { + "title": "با ما در تماس باشید", + "subtitle": "خوشحال می‌شویم از شما بشنویم. اگر پرسش، بازخورد یا درخواست قابلیتی دارید، لطفاً در تماس با ما درنگ نکنید.", + "email": "می‌توانید مستقیماً از طریق ایمیل با ما در تماس باشید:" + }, + "licensing": { + "title": "مجوزها برای", + "subtitle": "مجوزی را برگزینید که با نیازهای شما همخوانی دارد." + }, + "multiTool": { + "uploadPdfs": "بارگذاری PDFها", + "upload": "بارگذاری", + "addBlankPage": "افزودن صفحهٔ خالی", + "edit": "ویرایش:", + "undo": "واگرد", + "redo": "ازنو", + "reset": "بازنشانی", + "selection": "انتخاب:", + "selectAll": "انتخاب همه", + "deselectAll": "لغو انتخاب همه", + "rotate": "چرخش:", + "rotateLeft": "چپ", + "rotateRight": "راست", + "transform": "دگرگونی:", + "duplicate": "تکثیر", + "split": "تقسیم", + "clear": "پاک‌سازی:", + "delete": "حذف", + "download": "دانلود:", + "downloadSelected": "دانلود موارد انتخاب‌شده", + "exportPdf": "برون‌ریزی PDF", + "uploadPdfFiles": "فایل‌های PDF یا تصویر را انتخاب کنید", + "dragAndDrop": "فایل‌های PDF یا تصویر را اینجا بکشید و رها کنید، یا برای انتخاب کلیک کنید", + "selectFiles": "انتخاب فایل‌ها", + "dropToAdd": "برای افزودن، فایل‌های PDF یا تصویر را رها کنید", + "convertingImages": "در حال تبدیل تصاویر به PDF…", + "failedToConvertImages": "تبدیل تصاویر به PDF ناموفق بود.", + "renderingPages": "در حال ترسیم صفحات…", + "actions": { + "duplicatePage": "تکثیر این صفحه", + "deletePage": "حذف این صفحه", + "insertPdf": "درج PDF پس از این صفحه", + "toggleSplit": "تغییر وضعیت تقسیم پس از این صفحه" + }, + "pleaseWait": "لطفاً صبر کنید", + "pagesRendering": "صفحات هنوز در حال ترسیم هستند. لطفاً صبر کنید…", + "noPagesSelected": "هیچ صفحه‌ای انتخاب نشده است", + "selectOnePage": "لطفاً دست‌کم یک صفحه را برای دانلود انتخاب کنید.", + "noPages": "بدون صفحه", + "noPagesToExport": "هیچ صفحه‌ای برای برون‌ریزی وجود ندارد.", + "renderingTitle": "در حال ترسیم پیش‌نمایش صفحات", + "errorRendering": "ترسیم بندانگشتی صفحات ناموفق بود", + "error": "خطا", + "failedToLoad": "بارگذاری ناموفق بود" + }, + "simpleMode": { + "title": "ابزارهای PDF", + "subtitle": "برای شروع، یک ابزار برگزینید" + }, + "disabledTool": { + "title": "ابزار در دسترس نیست", + "heading": "این ابزار غیرفعال شده است", + "message": "این ابزار در نسخهٔ مستقرشدهٔ شما در دسترس نیست. برای اطلاعات بیشتر با مدیر سیستم خود تماس بگیرید.", + "backHome": "بازگشت به خانه" + } +} diff --git a/public/locales/fa/tools.json b/public/locales/fa/tools.json new file mode 100644 index 00000000..6496e516 --- /dev/null +++ b/public/locales/fa/tools.json @@ -0,0 +1,1154 @@ +{ + "categories": { + "popularTools": "ابزارهای پرکاربرد", + "editAnnotate": "ویرایش و حاشیه‌نویسی", + "convertToPdf": "تبدیل به PDF", + "convertFromPdf": "تبدیل از PDF", + "organizeManage": "سازماندهی و مدیریت", + "optimizeRepair": "بهینه‌سازی و ترمیم", + "securePdf": "ایمن‌سازی PDF", + "input": "ورودی", + "output": "خروجی" + }, + "convertToPdf": "تبدیل به PDF", + "pdfMultiTool": { + "name": "ابزار چندکارهٔ PDF", + "subtitle": "ادغام، تقسیم، سازماندهی، حذف، چرخش، افزودن صفحهٔ خالی، استخراج و تکثیر در یک رابط یکپارچه." + }, + "mergePdf": { + "name": "ادغام PDF", + "subtitle": "چند PDF را در یک فایل ترکیب کنید. نشانک‌ها حفظ می‌شوند.", + "retainPageLabels": "حفظ برچسب‌های اصلی صفحات هر فایل", + "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید)." + }, + "splitPdf": { + "name": "تقسیم PDF", + "subtitle": "بازه‌ای از صفحات را در یک PDF تازه استخراج کنید." + }, + "compressPdf": { + "name": "فشرده‌سازی PDF", + "subtitle": "حجم فایل PDF خود را کاهش دهید.", + "algorithmLabel": "الگوریتم فشرده‌سازی", + "condense": "Condense (پیشنهادی)", + "photon": "Photon (برای PDFهای پرتصویر)", + "condenseInfo": "Condense از فشرده‌سازی پیشرفته بهره می‌گیرد: حذف داده‌های زائد، بهینه‌سازی تصاویر و کاهش قلم‌ها. مناسب برای بیشتر PDFها.", + "photonInfo": "Photon صفحات را به تصویر تبدیل می‌کند. برای PDFهای پرتصویر یا اسکن‌شده به کار می‌رود.", + "photonWarning": "هشدار: متن دیگر قابل انتخاب نخواهد بود و پیوندها از کار می‌افتند.", + "levelLabel": "سطح فشرده‌سازی", + "light": "سبک (حفظ کیفیت)", + "balanced": "متعادل (پیشنهادی)", + "aggressive": "تهاجمی (فایل‌های کوچک‌تر)", + "extreme": "بیشینه (حداکثر فشرده‌سازی)", + "grayscale": "تبدیل به خاکستری", + "grayscaleHint": "با حذف اطلاعات رنگ، حجم فایل را کاهش می‌دهد", + "customSettings": "تنظیمات سفارشی", + "customSettingsHint": "تنظیم دقیق پارامترهای فشرده‌سازی:", + "outputQuality": "کیفیت خروجی", + "resizeImagesTo": "تغییر اندازهٔ تصاویر به", + "onlyProcessAbove": "فقط پردازش موارد بالاتر از", + "removeMetadata": "حذف فراداده", + "subsetFonts": "کاهش قلم‌ها (حذف نویسه‌های بلااستفاده)", + "removeThumbnails": "حذف بندانگشتی‌های تعبیه‌شده", + "compressButton": "فشرده‌سازی PDF" + }, + "pdfEditor": { + "name": "ویرایشگر PDF", + "subtitle": "حاشیه‌نویسی، هایلایت، سانسور، درج نظر، افزودن شکل و تصویر، جستجو و مشاهدهٔ PDF." + }, + "jpgToPdf": { + "name": "JPG به PDF", + "subtitle": "از تصاویر JPG، JPEG و JPEG2000 (JP2/JPX) یک PDF بسازید." + }, + "signPdf": { + "name": "امضای PDF", + "subtitle": "امضای خود را بکشید، تایپ کنید یا بارگذاری کنید." + }, + "cropPdf": { + "name": "برش PDF", + "subtitle": "حاشیهٔ همهٔ صفحات PDF خود را کوتاه کنید." + }, + "extractPages": { + "name": "استخراج صفحات", + "subtitle": "گزیده‌ای از صفحات را به‌صورت فایل‌های تازه ذخیره کنید." + }, + "duplicateOrganize": { + "name": "تکثیر و سازماندهی", + "subtitle": "صفحات را تکثیر، مرتب و حذف کنید.", + "saveChanges": "ذخیرهٔ تغییرات" + }, + "deletePages": { + "name": "حذف صفحات", + "subtitle": "صفحات مشخصی را از سند خود بردارید." + }, + "editBookmarks": { + "name": "ویرایش نشانک‌ها", + "subtitle": "نشانک‌های PDF را افزودن، ویرایش، درون‌ریزی، حذف و استخراج کنید.", + "autoExtractExisting": "استخراج خودکار نشانک‌های موجود", + "importSectionTitle": "یا نشانک‌ها را درون‌ریزی کنید", + "csvFormat": "قالب CSV: عنوان،صفحه،سطح", + "jsonFormat": "قالب JSON", + "uploadCsvAction": "برای بارگذاری CSV کلیک کنید", + "uploadJsonAction": "برای بارگذاری JSON کلیک کنید", + "orDragHere": "یا اینجا بکشید" + }, + "tableOfContents": { + "name": "فهرست مطالب", + "subtitle": "از روی نشانک‌های PDF یک صفحهٔ فهرست مطالب بسازید.", + "titleLabel": "عنوان فهرست مطالب", + "fontSize": "اندازهٔ قلم", + "fontFamily": "خانوادهٔ قلم", + "addBookmark": "افزودن نشانک برای صفحهٔ فهرست", + "generateButton": "ساخت فهرست مطالب", + "selectPdfFirst": "لطفاً نخست یک فایل PDF برگزینید.", + "statusReadingFileMainThread": "در حال خواندن فایل (نخ اصلی)…", + "statusGenerating": "در حال ساخت فهرست مطالب…", + "errorReadingFileWithMessage": "خطا در خواندن فایل: {{message}}", + "successGeneratedDownloadStarted": "فهرست مطالب با موفقیت ساخته شد! دانلود آغاز شد.", + "workerErrorWithMessage": "خطا: {{message}}", + "workerErrorOccurred": "خطای worker رخ داد. برای جزئیات، کنسول را بررسی کنید." + }, + "pageNumbers": { + "name": "شماره‌گذاری صفحات", + "subtitle": "شمارهٔ صفحات را در سند خود درج کنید." + }, + "addPageLabels": { + "name": "افزودن برچسب صفحه", + "subtitle": "برچسب‌های صفحهٔ PDF را با اعداد رومی، پیشوند و نقطهٔ شروع دلخواه اعمال کنید.", + "uploadHint": "فایل PDF", + "howLabelsWorkTitle": "برچسب‌ها چگونه کار می‌کنند", + "howLabelsWorkStep1": "برای برچسب‌گذاری همهٔ صفحات، بازهٔ صفحات را خالی بگذارید.", + "howLabelsWorkStep2": "برای صفحات آغازین از سبک‌هایی مانند اعداد رومی و برای متن اصلی از شماره‌گذاری اعشاری استفاده کنید.", + "howLabelsWorkStep3": "برای ساخت برچسب‌هایی مانند A-0، A-1، A-2 از پیشوند همراه با مقدار شروع ۰ استفاده کنید.", + "howLabelsWorkStep4": "هرگاه یک قاعده از بازه‌ای ناپیوسته مانند ۱-۹،۳۰-۴۰ یا فرد استفاده می‌کند، پیوستگی شماره‌گذاری را فعال کنید.", + "labelRulesTitle": "قواعد برچسب", + "addRule": "افزودن قاعده", + "removeExistingLabels": "حذف برچسب‌های موجود صفحات پیش از اعمال این قواعد", + "cpdfNote": "این ابزار از CoherentPDF در مرورگر شما استفاده می‌کند. اگر CoherentPDF پیکربندی نشده باشد، از شما خواسته می‌شود آن را در تنظیمات WASM راه‌اندازی کنید.", + "processButton": "افزودن برچسب صفحه", + "howItWorksUploadTitle": "بارگذاری فایل", + "howItWorksUploadDescription": "PDF‌ای را که می‌خواهید دوباره برچسب‌گذاری کنید برگزینید.", + "howItWorksDefineTitle": "تعریف قواعد", + "howItWorksDefineDescription": "یک یا چند قاعدهٔ برچسب صفحه را با بازهٔ صفحات، سبک، پیشوند و مقدار شروع تعیین کنید.", + "howItWorksDownloadTitle": "دانلود", + "howItWorksDownloadDescription": "PDF به‌روزشده را با برچسب‌های تازهٔ صفحاتش ذخیره کنید.", + "relatedToolsTitle": "ابزارهای مرتبط PDF", + "faqWhatIsQuestion": "برچسب صفحه چیست؟", + "faqWhatIsAnswer": "برچسب صفحه همان شماره‌ای است که خوانندگان PDF در بندانگشتی‌ها، پیمایش صفحات و پنجرهٔ چاپ نشان می‌دهند. این برچسب متن تازه‌ای روی خودِ صفحه چاپ نمی‌کند.", + "faqMixQuestion": "آیا می‌توانم اعداد رومی و اعداد معمولی را با هم به کار ببرم؟", + "faqMixAnswer": "بله. چند قاعده بیفزایید؛ مثلاً ۱-۴ با LowercaseRoman و ۵-۲۰ با DecimalArabic.", + "faqPrivacyQuestion": "آیا فایل‌های من خصوصی می‌مانند؟", + "faqPrivacyAnswer": "بله. پردازش در مرورگر شما و با CoherentPDF WASM انجام می‌شود. PDF شما از دستگاهتان خارج نمی‌شود.", + "invalidFileTitle": "فایل نامعتبر", + "invalidFileMessage": "لطفاً یک فایل PDF معتبر بارگذاری کنید.", + "loadingPdf": "در حال بارگذاری PDF…", + "protectedPdfTitle": "PDF محافظت‌شده", + "protectedPdfMessage": "این PDF با گذرواژه محافظت شده است. لطفاً نخست از ابزار رمزگشایی یا تغییر مجوزها استفاده کنید.", + "loadErrorMessage": "بارگذاری فایل PDF ناموفق بود. ممکن است فایل نامعتبر، آسیب‌دیده یا محافظت‌شده با گذرواژه باشد.", + "fileMeta": "{{size}} • {{count}} صفحه", + "ruleTitle": "قاعدهٔ برچسب {{number}}", + "pageRangeLabel": "بازهٔ صفحات", + "pageRangePlaceholder": "همهٔ صفحات، یا مثلاً ۱-۴، ۷، فرد", + "labelStyleLabel": "سبک برچسب", + "labelPrefixLabel": "پیشوند برچسب", + "labelPrefixPlaceholder": "پیشوند اختیاری، مثلاً -A", + "startValueLabel": "مقدار شروع", + "continueNumbering": "ادامهٔ شماره‌گذاری در بازه‌های ناپیوسته", + "examplesNote": "نمونه‌ها: ۱-۴ برای صفحات آغازین رومی، ۱۵-۲۰ با پیشوند -A و مقدار شروع ۰، یا فرد با پیوستگی فعال.", + "uploadFirstMessage": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "applyingLabels": "در حال اعمال برچسب‌های صفحه…", + "invalidRangeMessage": "قاعدهٔ {{number}} بازهٔ صفحات نامعتبری دارد: {{range}}", + "allPages": "همهٔ صفحات", + "emptyOutputMessage": "CoherentPDF یک فایل خالی تولید کرد.", + "successMessage": "برچسب‌های صفحه با موفقیت افزوده شدند!", + "processErrorMessage": "افزودن برچسب‌های صفحه ممکن نشد.", + "styleOptions": { + "DecimalArabic": "اعشاری عربی", + "LowercaseRoman": "رومی کوچک", + "UppercaseRoman": "رومی بزرگ", + "LowercaseLetters": "حروف کوچک", + "UppercaseLetters": "حروف بزرگ", + "NoLabelPrefixOnly": "بدون برچسب، فقط پیشوند" + } + }, + "batesNumbering": { + "name": "شماره‌گذاری Bates", + "subtitle": "شماره‌های ترتیبی Bates را در یک یا چند فایل PDF بیفزایید." + }, + "addWatermark": { + "name": "افزودن واترمارک", + "subtitle": "متن یا تصویری را روی صفحات PDF خود مهر کنید.", + "applyToAllPages": "اعمال بر همهٔ صفحات" + }, + "headerFooter": { + "name": "سرصفحه و پاصفحه", + "subtitle": "متن را به بالا و پایین صفحات بیفزایید." + }, + "invertColors": { + "name": "وارونه‌سازی رنگ‌ها", + "subtitle": "نسخه‌ای با «حالت تاریک» از PDF خود بسازید." + }, + "scannerEffect": { + "name": "جلوهٔ اسکنر", + "subtitle": "PDF خود را شبیه یک سند اسکن‌شده کنید.", + "scanSettings": "تنظیمات اسکن", + "colorspace": "فضای رنگی", + "gray": "خاکستری", + "border": "حاشیه", + "rotate": "چرخش", + "rotateVariance": "پراکندگی چرخش", + "brightness": "روشنایی", + "contrast": "کنتراست", + "blur": "محو‌شدگی", + "noise": "نویز", + "yellowish": "متمایل به زرد", + "resolution": "وضوح", + "processButton": "اعمال جلوهٔ اسکنر" + }, + "adjustColors": { + "name": "تنظیم رنگ‌ها", + "subtitle": "روشنایی، کنتراست، اشباع و موارد دیگر را در PDF خود تنظیم دقیق کنید.", + "colorSettings": "تنظیمات رنگ", + "brightness": "روشنایی", + "contrast": "کنتراست", + "saturation": "اشباع", + "hueShift": "جابه‌جایی فام", + "temperature": "دمای رنگ", + "tint": "ته‌رنگ", + "gamma": "گاما", + "sepia": "سپیا", + "processButton": "اعمال تنظیمات رنگ" + }, + "backgroundColor": { + "name": "رنگ پس‌زمینه", + "subtitle": "رنگ پس‌زمینهٔ PDF خود را تغییر دهید." + }, + "changeTextColor": { + "name": "تغییر رنگ متن", + "subtitle": "رنگ متن PDF خود را تغییر دهید." + }, + "addStamps": { + "name": "افزودن مهر", + "subtitle": "با نوار ابزار حاشیه‌نویسی، مهرهای تصویری به PDF خود بیفزایید.", + "usernameLabel": "نام کاربری مهر", + "usernamePlaceholder": "نام خود را وارد کنید (برای مهرها)", + "usernameHint": "این نام روی مهرهایی که می‌سازید نمایان می‌شود.", + "saveButton": "ذخیرهٔ PDF مهرخورده" + }, + "removeAnnotations": { + "name": "حذف حاشیه‌نویسی‌ها", + "subtitle": "نظرها، هایلایت‌ها و پیوندها را بردارید." + }, + "pdfFormFiller": { + "name": "پرکنندهٔ فرم PDF", + "subtitle": "فرم‌ها را مستقیماً در مرورگر پر کنید. از فرم‌های XFA نیز پشتیبانی می‌شود.", + "uploadHint": "یک PDF دارای فیلدهای فرم بارگذاری کنید. آن‌ها را مستقیماً در نمایشگر زیر پر کنید، سپس برای ذخیره و دانلود فرم تکمیل‌شده روی دکمه کلیک کنید. از فرم‌های XFA نیز پشتیبانی می‌شود.", + "xfaNoteTitle": "نکته دربارهٔ فرم‌های XFA:", + "xfaNoteDescription": "XFA (معماری فرم‌های XML) قالبی قدیمی است که تنها برخی نمایشگرهای PDF مانند BentoPDF و Firefox از آن پشتیبانی می‌کنند. اگر یک PDF از نوع XFA را در نرم‌افزار دیگری باز کنید و صفحات خالی یا بدون فیلد فرم ببینید، یعنی آن نمایشگر از XFA پشتیبانی نمی‌کند. برای مشاهده و تکمیل درست فرم‌های XFA، از Firefox یا پرکنندهٔ فرم BentoPDF استفاده کنید." + }, + "createPdfForm": { + "name": "ساخت فرم PDF", + "subtitle": "با فیلدهای متنی کشیدنی‌رهاکردنی، فرم‌های PDF قابل تکمیل بسازید.", + "orSeparator": "یا", + "createBlankPdf": "ساخت PDF خالی" + }, + "removeBlankPages": { + "name": "حذف صفحات خالی", + "subtitle": "صفحات خالی را به‌طور خودکار تشخیص داده و حذف کنید.", + "sensitivityHint": "بالاتر = سخت‌گیرانه‌تر، فقط صفحات کاملاً خالی. پایین‌تر = صفحات دارای اندکی محتوا را نیز در بر می‌گیرد." + }, + "imageToPdf": { + "name": "تصاویر به PDF", + "subtitle": "تبدیل JPG، PNG، BMP، GIF، TIFF، PNM، PGM، PBM، PPM، PAM، JXR، JPX، JP2، PSD، SVG، HEIC و WebP به PDF." + }, + "pngToPdf": { + "name": "PNG به PDF", + "subtitle": "از یک یا چند تصویر PNG یک PDF بسازید." + }, + "webpToPdf": { + "name": "WebP به PDF", + "subtitle": "از یک یا چند تصویر WebP یک PDF بسازید." + }, + "svgToPdf": { + "name": "SVG به PDF", + "subtitle": "از یک یا چند تصویر SVG یک PDF بسازید." + }, + "bmpToPdf": { + "name": "BMP به PDF", + "subtitle": "از یک یا چند تصویر BMP یک PDF بسازید." + }, + "heicToPdf": { + "name": "HEIC به PDF", + "subtitle": "از یک یا چند تصویر HEIC یک PDF بسازید." + }, + "tiffToPdf": { + "name": "TIFF به PDF", + "subtitle": "از یک یا چند تصویر TIFF یک PDF بسازید." + }, + "textToPdf": { + "name": "متن به PDF", + "subtitle": "یک فایل متنی ساده را به PDF تبدیل کنید.", + "uploadFiles": "بارگذاری فایل‌ها", + "typeText": "تایپ متن", + "acceptedFormats": "فایل‌های متنی (.txt)", + "fontFamily": "خانوادهٔ قلم", + "fontSize": "اندازهٔ قلم", + "textColor": "رنگ متن", + "pageSize": "اندازهٔ صفحه", + "rtlAutoDetectNote": "زبان‌های راست‌به‌چپ (عربی، اردو، فارسی و غیره) به‌طور خودکار تشخیص داده می‌شوند و جهت متن تنظیم می‌گردد.", + "createButton": "ساخت PDF" + }, + "jsonToPdf": { + "name": "JSON به PDF", + "subtitle": "فایل‌های JSON را به قالب PDF تبدیل کنید.", + "noteLabel": "نکته:", + "noteText": "تنها فایل‌های JSON ساخته‌شده با ابزار تبدیل PDF به JSON پشتیبانی می‌شوند. فایل‌های JSON استاندارد از ابزارهای دیگر کار نخواهند کرد.", + "acceptedFormats": "چند فایل JSON", + "status": { + "selectAtLeastOne": "لطفاً دست‌کم ۱ فایل JSON برگزینید", + "selectedReady": "{{count}} فایل برگزیده شد. آمادهٔ تبدیل!", + "readingFiles": "در حال خواندن فایل‌ها (نخ اصلی)…", + "converting": "در حال تبدیل فایل‌های JSON به PDF…", + "readError": "خطا در خواندن فایل‌ها: {{message}}", + "creatingZip": "در حال ساخت فایل ZIP…", + "success": "فایل‌های JSON با موفقیت به PDF تبدیل شدند! دانلود ZIP آغاز شد.", + "zipError": "خطا در ساخت ZIP: {{message}}", + "workerError": "خطای Worker: {{message}}", + "getStarted": "برای شروع، فایل‌های JSON را برگزینید" + } + }, + "pdfToJpg": { + "name": "PDF به JPG", + "subtitle": "هر صفحهٔ PDF را به یک تصویر JPG تبدیل کنید.", + "imageQuality": "کیفیت تصویر", + "imageQualityExplanation": "کیفیت بالاتر = حجم فایل بیشتر", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به JPG تبدیل شد!", + "conversionError": "تبدیل PDF به JPG ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + }, + "loader": { + "converting": "در حال تبدیل به JPG…" + } + }, + "pdfToPng": { + "name": "PDF به PNG", + "subtitle": "هر صفحهٔ PDF را به یک تصویر PNG تبدیل کنید.", + "imageScale": "مقیاس تصویر", + "imageScaleExplanation": "مقیاس بالاتر = کیفیت بهتر اما حجم فایل بیشتر", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به PNG تبدیل شد!", + "conversionError": "تبدیل PDF به PNG ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + }, + "loader": { + "converting": "در حال تبدیل به PNG…" + } + }, + "pdfToWebp": { + "name": "PDF به WebP", + "subtitle": "هر صفحهٔ PDF را به یک تصویر WebP تبدیل کنید.", + "imageQuality": "کیفیت تصویر", + "imageQualityExplanation": "کیفیت بالاتر = حجم فایل بیشتر", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به WebP تبدیل شد!", + "conversionError": "تبدیل PDF به WebP ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + }, + "loader": { + "converting": "در حال تبدیل به WebP…" + } + }, + "pdfToBmp": { + "name": "PDF به BMP", + "subtitle": "هر صفحهٔ PDF را به یک تصویر BMP تبدیل کنید.", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به BMP تبدیل شد!", + "conversionError": "تبدیل PDF به BMP ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + }, + "loader": { + "converting": "در حال تبدیل به BMP…" + } + }, + "pdfToTiff": { + "name": "PDF به TIFF", + "subtitle": "هر صفحهٔ PDF را به یک تصویر TIFF تبدیل کنید.", + "dpi": "DPI (وضوح)", + "dpiExplanation": "DPI بالاتر = کیفیت بهتر برای چاپ، حجم فایل بیشتر", + "compression": "فشرده‌سازی", + "compressionExplanation": "LZW و Deflate بدون اتلاف هستند. CCITT Group 4 برای اسناد اسکن‌شدهٔ سیاه‌وسفید بهترین گزینه است.", + "colorMode": "حالت رنگ", + "multiPage": "ذخیره به‌صورت TIFF چندصفحه‌ای (یک فایل)", + "loadingVips": "در حال بارگذاری پردازشگر تصویر…", + "converting": "در حال تبدیل به TIFF…", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به TIFF تبدیل شد!", + "conversionError": "تبدیل PDF به TIFF ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + } + }, + "pdfToCbz": { + "name": "PDF به CBZ", + "subtitle": "یک PDF را به فایل CBZ (بایگانی کتاب کمیک) برای خوانندگان کمیک تبدیل کنید.", + "imageFormat": "قالب تصویر", + "quality": "کیفیت تصویر", + "qualityExplanation": "کیفیت بالاتر = حجم فایل بیشتر", + "scale": "مقیاس", + "scaleExplanation": "مقیاس بالاتر = کیفیت بهتر برای نمایشگرهای پروضوح", + "grayscale": "تبدیل به خاکستری", + "manga": "حالت مانگا (راست به چپ)", + "includeMetadata": "گنجاندن فرادادهٔ ComicInfo.xml", + "titleLabel": "عنوان", + "seriesLabel": "مجموعه", + "authorLabel": "نویسنده(ها)", + "numberLabel": "شماره", + "volumeLabel": "جلد", + "publisherLabel": "ناشر", + "tagsLabel": "برچسب‌ها / ژانر", + "yearLabel": "سال انتشار", + "ratingLabel": "امتیاز (۰-۵)", + "converting": "در حال تبدیل به CBZ…", + "alert": { + "invalidFile": "فایل نامعتبر", + "invalidFileExplanation": "لطفاً یک فایل PDF بارگذاری کنید.", + "noFile": "بدون فایل", + "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "conversionSuccess": "PDF با موفقیت به CBZ تبدیل شد!", + "conversionError": "تبدیل PDF به CBZ ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + } + }, + "pdfToGreyscale": { + "name": "PDF به خاکستری", + "subtitle": "همهٔ رنگ‌ها را به سیاه‌وسفید تبدیل کنید." + }, + "pdfToJson": { + "name": "PDF به JSON", + "subtitle": "فایل‌های PDF را به قالب JSON تبدیل کنید.", + "convertButton": "تبدیل به JSON", + "status": { + "selectAtLeastOne": "لطفاً دست‌کم ۱ فایل PDF برگزینید", + "selectedReady": "{{count}} فایل برگزیده شد. آمادهٔ تبدیل!", + "checkingEncrypted": "در حال بررسی PDFهای رمزگذاری‌شده…", + "readingFiles": "در حال خواندن فایل‌ها (نخ اصلی)…", + "converting": "در حال تبدیل PDFها به JSON…", + "readError": "خطا در خواندن فایل‌ها: {{message}}", + "creatingZip": "در حال ساخت فایل ZIP…", + "success": "PDFها با موفقیت به JSON تبدیل شدند! دانلود ZIP آغاز شد.", + "zipError": "خطا در ساخت ZIP: {{message}}", + "workerError": "خطای Worker: {{message}}", + "getStarted": "برای شروع، فایل‌های PDF را برگزینید" + } + }, + "ocrPdf": { + "name": "OCR روی PDF", + "subtitle": "PDF را قابل جستجو و کپی کنید.", + "languagesInDocument": "زبان‌های موجود در سند", + "searchLanguagesPlaceholder": "جستجوی زبان‌ها…", + "selectedLabel": "برگزیده:", + "none": "هیچ‌کدام", + "advancedSettingsSummary": "تنظیمات پیشرفته (برای بهبود دقت پیشنهاد می‌شود)", + "resolutionLabel": "وضوح", + "resolutionStandard": "استاندارد (۱۹۲ DPI)", + "resolutionHigh": "بالا (۲۸۸ DPI)", + "resolutionUltra": "فوق‌العاده (۳۸۴ DPI)", + "binarizeLabel": "دوسطحی‌سازی تصویر (افزایش کنتراست برای اسکن‌های تمیز)", + "embedFullFontsLabel": "تعبیهٔ کامل قلم‌ها (فایل بزرگ‌تر، سازگاری بهتر)", + "whitelistPresetLabel": "پیش‌تنظیم فهرست سفید نویسه‌ها", + "whitelistNone": "هیچ‌کدام (همهٔ نویسه‌ها)", + "whitelistAlphanumeric": "حروف و ارقام + نشانه‌گذاری پایه", + "whitelistNumbersCurrency": "اعداد + نمادهای ارز", + "whitelistLettersOnly": "فقط حروف (A-Z، a-z)", + "whitelistNumbersOnly": "فقط اعداد (0-9)", + "whitelistInvoice": "فاکتور/رسید (اعداد، $، .، -، /)", + "whitelistForms": "فرم‌ها (حروف و ارقام + نمادهای رایج)", + "whitelistCustom": "سفارشی…", + "whitelistHelp": "تنها این نویسه‌ها شناسایی می‌شوند. برای همهٔ نویسه‌ها، خالی بگذارید.", + "whitelistInputLabel": "فهرست سفید نویسه‌ها (اختیاری)", + "whitelistInputPlaceholder": "مثلاً abcdefghijklmnopqrstuvwxyz0123456789$.,", + "startButton": "شروع OCR", + "progressInitializing": "در حال راه‌اندازی…", + "resultsTitle": "OCR به پایان رسید", + "resultsDescription": "PDF قابل جستجوی شما آماده است. همچنین می‌توانید متن استخراج‌شدهٔ زیر را کپی یا دانلود کنید.", + "copyToClipboard": "کپی در حافظهٔ موقت", + "downloadTxt": "دانلود به‌صورت ‎.txt", + "downloadSearchablePdf": "دانلود PDF قابل جستجو", + "statusPrefix": "وضعیت: {{status}}", + "availabilityInvalidConfig": "این نسخهٔ مستقرشده هیچ زبان OCR معتبری ارائه نمی‌دهد. آن را با تنظیم VITE_TESSERACT_AVAILABLE_LANGUAGES روی کدهای معتبر Tesseract بازسازی کنید.", + "availabilityBundles": "این نسخهٔ مستقرشده OCR را برای این زبان‌ها همراه دارد: {{languages}}.", + "noLanguagesAvailable": "هیچ زبان OCR‌ای در این نسخهٔ مستقرشده در دسترس نیست.", + "noLanguagesTitle": "هیچ زبانی برگزیده نشده است", + "noLanguagesMessage": "لطفاً دست‌کم یک زبان برای OCR برگزینید.", + "noFileTitle": "بدون فایل", + "noFileMessage": "لطفاً نخست یک فایل PDF بارگذاری کنید.", + "languageUnavailableTitle": "زبان OCR در دسترس نیست", + "ocrErrorTitle": "خطای OCR", + "ocrErrorMessage": "در فرایند OCR خطایی رخ داد. ممکن است worker بارگذاری نشده باشد. لطفاً دوباره تلاش کنید.", + "howItWorksTitle": "چگونه کار می‌کند:", + "extractTextLabel": "استخراج متن:", + "extractTextDescription": "از Tesseract OCR برای شناسایی متن در تصاویر یا PDFهای اسکن‌شده بهره می‌گیرد.", + "searchableOutputLabel": "خروجی قابل جستجو:", + "searchableOutputDescription": "یک PDF تازه با لایهٔ متنی نامرئی می‌سازد که سند شما را کاملاً قابل جستجو می‌کند و در عین حال ظاهر اصلی را نگه می‌دارد.", + "characterFilteringLabel": "پالایش نویسه‌ها:", + "characterFilteringDescription": "با فهرست سفید، نویسه‌های ناخواسته را کنار بگذارید و دقت را برای انواع خاص سند (فاکتور، فرم و غیره) بهبود دهید.", + "multiLanguageSupportLabel": "پشتیبانی چندزبانه:", + "multiLanguageSupportDescription": "برای اسنادی که محتوای چندزبانه دارند، چند زبان را برگزینید." + }, + "alternateMerge": { + "name": "ادغام یک‌درمیان صفحات", + "subtitle": "PDFها را با درج یک‌درمیان صفحات هر فایل ادغام کنید. نشانک‌ها حفظ می‌شوند.", + "retainPageLabels": "حفظ برچسب‌های اصلی صفحات هر فایل", + "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید)." + }, + "addAttachments": { + "name": "افزودن پیوست", + "subtitle": "یک یا چند فایل را در PDF خود تعبیه کنید." + }, + "extractAttachments": { + "name": "استخراج پیوست‌ها", + "subtitle": "همهٔ فایل‌های تعبیه‌شده در PDF(ها) را به‌صورت ZIP استخراج کنید." + }, + "editAttachments": { + "name": "ویرایش پیوست‌ها", + "subtitle": "پیوست‌های PDF خود را مشاهده یا حذف کنید." + }, + "dividePages": { + "name": "تقسیم صفحات", + "subtitle": "صفحات را به‌صورت افقی یا عمودی تقسیم کنید." + }, + "addBlankPage": { + "name": "افزودن صفحهٔ خالی", + "subtitle": "یک صفحهٔ خالی را در هر جای PDF خود درج کنید." + }, + "reversePages": { + "name": "وارونه‌سازی ترتیب صفحات", + "subtitle": "ترتیب همهٔ صفحات سند خود را وارونه کنید." + }, + "rotatePdf": { + "name": "چرخاندن PDF", + "subtitle": "صفحات را در گام‌های ۹۰ درجه بچرخانید." + }, + "rotateCustom": { + "name": "چرخاندن با زاویهٔ دلخواه", + "subtitle": "صفحات را با هر زاویهٔ دلخواهی بچرخانید." + }, + "nUpPdf": { + "name": "N-Up کردن PDF", + "subtitle": "چند صفحه را روی یک برگ بچینید." + }, + "combineToSinglePage": { + "name": "ترکیب در یک صفحه", + "subtitle": "همهٔ صفحات را در یک پیمایش پیوسته به هم بدوزید." + }, + "viewMetadata": { + "name": "مشاهدهٔ فراداده", + "subtitle": "ویژگی‌های پنهان PDF خود را بررسی کنید." + }, + "editMetadata": { + "name": "ویرایش فراداده", + "subtitle": "نویسنده، عنوان و دیگر ویژگی‌ها را تغییر دهید." + }, + "pdfsToZip": { + "name": "PDFها به ZIP", + "subtitle": "چند فایل PDF را در یک بایگانی ZIP بسته‌بندی کنید." + }, + "comparePdfs": { + "name": "مقایسهٔ PDFها", + "subtitle": "دو PDF را کنار هم مقایسه کنید.", + "firstPdf": "PDF نخست", + "secondPdf": "PDF دوم", + "clickOrDrop": "کلیک کنید یا رها کنید", + "page": "صفحه", + "overlay": "روی‌هم", + "sideBySide": "کنار هم", + "flicker": "چشمک‌زن", + "syncScroll": "پیمایش هم‌زمان", + "export": "برون‌ریزی", + "exportAsPdf": "برون‌ریزی به‌صورت PDF", + "splitView": "نمای تقسیم‌شده", + "alternating": "یک‌درمیان", + "leftDocument": "سند چپ", + "rightDocument": "سند راست", + "original": "اصلی", + "modified": "تغییریافته", + "searchChanges": "جستجوی تغییرات…", + "deleted": "حذف‌شده", + "added": "افزوده‌شده", + "prevPage": "صفحهٔ پیشین", + "nextPage": "صفحهٔ بعدی", + "prevChange": "تغییر پیشین", + "nextChange": "تغییر بعدی", + "uploadTwoPdfs": "برای دیدن تفاوت‌ها، دو PDF بارگذاری کنید.", + "noDifferences": "هیچ تفاوتی در این صفحه یافت نشد.", + "noMatchingChanges": "هیچ تغییری با پالایهٔ کنونی همخوانی ندارد.", + "pageNotExist": "صفحهٔ {{page}} در این PDF وجود ندارد.", + "noPairedPage": "برای این سمت هیچ صفحهٔ متناظری وجود ندارد.", + "buildingModel": "در حال ساخت مدل تطبیق صفحات…", + "indexingPdf": "در حال نمایه‌سازی PDF شمارهٔ {{num}}، صفحهٔ {{page}} از {{total}}…", + "loadingComparison": "در حال بارگذاری مقایسهٔ {{current}} از {{total}}…", + "runningOcr": "در حال اجرای OCR روی صفحهٔ {{page}}…", + "preparingExport": "در حال آماده‌سازی برون‌ریزی PDF…", + "renderingPage": "در حال ترسیم صفحهٔ {{current}} از {{total}}…", + "exportError": "خطای برون‌ریزی", + "exportFailed": "برون‌ریزی PDF مقایسه ممکن نشد.", + "loadingFile": "در حال بارگذاری {{name}}…", + "invalidFile": "فایل نامعتبر", + "invalidFileMsg": "لطفاً یک فایل PDF معتبر برگزینید.", + "loadError": "بارگذاری PDF ممکن نشد. ممکن است آسیب‌دیده یا محافظت‌شده با گذرواژه باشد." + }, + "posterizePdf": { + "name": "پوستری‌کردن PDF", + "subtitle": "یک صفحهٔ بزرگ را به چند صفحهٔ کوچک‌تر تقسیم کنید." + }, + "fixPageSize": { + "name": "اصلاح اندازهٔ صفحه", + "subtitle": "همهٔ صفحات را به اندازه‌ای یکسان استاندارد کنید." + }, + "linearizePdf": { + "name": "خطی‌سازی PDF", + "subtitle": "PDF را برای نمایش سریع در وب بهینه کنید." + }, + "pageDimensions": { + "name": "ابعاد صفحه", + "subtitle": "اندازه، جهت و واحد صفحات را تحلیل کنید." + }, + "removeRestrictions": { + "name": "حذف محدودیت‌ها", + "subtitle": "محافظت با گذرواژه و محدودیت‌های امنیتی مرتبط با فایل‌های PDF امضاشدهٔ دیجیتال را بردارید." + }, + "repairPdf": { + "name": "ترمیم PDF", + "subtitle": "داده‌ها را از فایل‌های PDF خراب یا آسیب‌دیده بازیابی کنید." + }, + "encryptPdf": { + "name": "رمزگذاری PDF", + "subtitle": "با افزودن گذرواژه، PDF خود را قفل کنید." + }, + "sanitizePdf": { + "name": "پاک‌سازی PDF", + "subtitle": "فراداده، حاشیه‌نویسی، اسکریپت و موارد دیگر را حذف کنید." + }, + "decryptPdf": { + "name": "رمزگشایی PDF", + "subtitle": "با برداشتن محافظت گذرواژه، PDF را باز کنید." + }, + "flattenPdf": { + "name": "مسطح‌سازی PDF", + "subtitle": "فیلدهای فرم و حاشیه‌نویسی‌ها را غیرقابل ویرایش کنید." + }, + "removeMetadata": { + "name": "حذف فراداده", + "subtitle": "داده‌های پنهان را از PDF خود بردارید." + }, + "changePermissions": { + "name": "تغییر مجوزها", + "subtitle": "مجوزهای کاربر را روی یک PDF تعیین یا تغییر دهید." + }, + "odtToPdf": { + "name": "ODT به PDF", + "subtitle": "فایل‌های متنی OpenDocument را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های ODT", + "convertButton": "تبدیل به PDF" + }, + "csvToPdf": { + "name": "CSV به PDF", + "subtitle": "فایل‌های صفحه‌گستردهٔ CSV را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های CSV", + "convertButton": "تبدیل به PDF" + }, + "rtfToPdf": { + "name": "RTF به PDF", + "subtitle": "اسناد Rich Text Format را به PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های RTF", + "convertButton": "تبدیل به PDF" + }, + "wordToPdf": { + "name": "Word به PDF", + "subtitle": "اسناد Word (DOCX، DOC، ODT، RTF) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های DOCX، DOC، ODT، RTF", + "convertButton": "تبدیل به PDF" + }, + "excelToPdf": { + "name": "Excel به PDF", + "subtitle": "صفحه‌گسترده‌های Excel (XLSX، XLS، ODS، CSV) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های XLSX، XLS، ODS، CSV", + "convertButton": "تبدیل به PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint به PDF", + "subtitle": "ارائه‌های PowerPoint (PPTX، PPT، ODP) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های PPTX، PPT، ODP", + "convertButton": "تبدیل به PDF" + }, + "markdownToPdf": { + "name": "Markdown به PDF", + "subtitle": "Markdown بنویسید یا بچسبانید و آن را به‌صورت یک PDF زیبا برون‌ریزی کنید.", + "paneMarkdown": "Markdown", + "panePreview": "پیش‌نمایش", + "btnUpload": "بارگذاری", + "btnSyncScroll": "پیمایش هم‌زمان", + "btnSettings": "تنظیمات", + "btnExportPdf": "برون‌ریزی PDF", + "settingsTitle": "تنظیمات Markdown", + "settingsPreset": "پیش‌تنظیم", + "presetDefault": "پیش‌فرض (شبیه GFM)", + "presetCommonmark": "CommonMark (سخت‌گیرانه)", + "presetZero": "کمینه (بدون قابلیت)", + "settingsOptions": "گزینه‌های Markdown", + "optAllowHtml": "اجازهٔ استفاده از تگ‌های HTML", + "optBreaks": "تبدیل خط تازه به
", + "optLinkify": "تبدیل خودکار نشانی‌ها به پیوند", + "optTypographer": "حروف‌نگار (گیومه‌های هوشمند و غیره)" + }, + "pdfBooklet": { + "name": "دفترچهٔ PDF", + "subtitle": "صفحات را برای چاپ دورو به‌صورت دفترچه بازچینی کنید. تا بزنید و منگنه کنید تا دفترچه ساخته شود.", + "howItWorks": "چگونه کار می‌کند:", + "step1": "یک فایل PDF بارگذاری کنید.", + "step2": "صفحات به ترتیب دفترچه‌ای بازچینی می‌شوند.", + "step3": "دورو چاپ کنید، از لبهٔ کوتاه برگردانید، تا بزنید و منگنه کنید.", + "paperSize": "اندازهٔ کاغذ", + "orientation": "جهت", + "portrait": "عمودی", + "landscape": "افقی", + "pagesPerSheet": "تعداد صفحه در هر برگ", + "createBooklet": "ساخت دفترچه", + "processing": "در حال پردازش…", + "pageCount": "در صورت نیاز، تعداد صفحات تا مضربی از ۴ تکمیل می‌شود." + }, + "xpsToPdf": { + "name": "XPS به PDF", + "subtitle": "اسناد XPS/OXPS را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های XPS، OXPS", + "convertButton": "تبدیل به PDF" + }, + "mobiToPdf": { + "name": "MOBI به PDF", + "subtitle": "کتاب‌های الکترونیکی MOBI را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های MOBI", + "convertButton": "تبدیل به PDF" + }, + "epubToPdf": { + "name": "EPUB به PDF", + "subtitle": "کتاب‌های الکترونیکی EPUB را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های EPUB", + "convertButton": "تبدیل به PDF" + }, + "fb2ToPdf": { + "name": "FB2 به PDF", + "subtitle": "کتاب‌های الکترونیکی FictionBook (FB2) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های FB2", + "convertButton": "تبدیل به PDF" + }, + "cbzToPdf": { + "name": "CBZ به PDF", + "subtitle": "بایگانی‌های کتاب کمیک (CBZ/CBR) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های CBZ، CBR", + "convertButton": "تبدیل به PDF" + }, + "wpdToPdf": { + "name": "WPD به PDF", + "subtitle": "اسناد WordPerfect (WPD) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های WPD", + "convertButton": "تبدیل به PDF" + }, + "wpsToPdf": { + "name": "WPS به PDF", + "subtitle": "اسناد WPS Office را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های WPS", + "convertButton": "تبدیل به PDF" + }, + "xmlToPdf": { + "name": "XML به PDF", + "subtitle": "اسناد XML را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های XML", + "convertButton": "تبدیل به PDF" + }, + "pagesToPdf": { + "name": "Pages به PDF", + "subtitle": "اسناد Apple Pages را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های Pages", + "convertButton": "تبدیل به PDF" + }, + "odgToPdf": { + "name": "ODG به PDF", + "subtitle": "فایل‌های گرافیکی OpenDocument (ODG) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های ODG", + "convertButton": "تبدیل به PDF" + }, + "odsToPdf": { + "name": "ODS به PDF", + "subtitle": "فایل‌های صفحه‌گستردهٔ OpenDocument (ODS) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های ODS", + "convertButton": "تبدیل به PDF" + }, + "odpToPdf": { + "name": "ODP به PDF", + "subtitle": "فایل‌های ارائهٔ OpenDocument (ODP) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های ODP", + "convertButton": "تبدیل به PDF" + }, + "pubToPdf": { + "name": "PUB به PDF", + "subtitle": "فایل‌های Microsoft Publisher (PUB) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های PUB", + "convertButton": "تبدیل به PDF" + }, + "vsdToPdf": { + "name": "VSD به PDF", + "subtitle": "فایل‌های Microsoft Visio (VSD، VSDX) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های VSD، VSDX", + "convertButton": "تبدیل به PDF" + }, + "psdToPdf": { + "name": "PSD به PDF", + "subtitle": "فایل‌های Adobe Photoshop (PSD) را به قالب PDF تبدیل کنید. از چند فایل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های PSD", + "convertButton": "تبدیل به PDF" + }, + "pdfToSvg": { + "name": "PDF به SVG", + "subtitle": "هر صفحهٔ یک فایل PDF را به گرافیک برداری مقیاس‌پذیر (SVG) تبدیل کنید تا در هر اندازه‌ای کیفیتی بی‌نقص داشته باشد." + }, + "extractTables": { + "name": "استخراج جدول‌های PDF", + "subtitle": "جدول‌ها را از فایل‌های PDF استخراج کرده و به‌صورت CSV، JSON یا Markdown برون‌ریزی کنید." + }, + "pdfToCsv": { + "name": "PDF به CSV", + "subtitle": "جدول‌ها را از PDF استخراج کرده و به قالب CSV تبدیل کنید." + }, + "pdfToExcel": { + "name": "PDF به Excel", + "subtitle": "جدول‌ها را از PDF استخراج کرده و به قالب Excel (XLSX) تبدیل کنید." + }, + "pdfToText": { + "name": "PDF به متن", + "subtitle": "متن را از فایل‌های PDF استخراج کرده و به‌صورت متن ساده (.txt) ذخیره کنید. از چند فایل پشتیبانی می‌شود.", + "noteLabel": "نکته:", + "noteText": "این ابزار تنها با PDFهایی کار می‌کند که به‌صورت دیجیتال ساخته شده‌اند. برای اسناد اسکن‌شده یا PDFهای مبتنی بر تصویر، به‌جای آن از ابزار OCR روی PDF استفاده کنید.", + "convertButton": "استخراج متن" + }, + "digitalSignPdf": { + "name": "امضای دیجیتال PDF", + "pageTitle": "امضای دیجیتال PDF - افزودن امضای رمزنگاری‌شده | BentoPDF", + "subtitle": "با گواهی‌های X.509 یک امضای دیجیتال رمزنگاری‌شده به PDF خود بیفزایید. از قالب‌های PKCS#12 (‎.pfx، ‎.p12) و PEM پشتیبانی می‌شود. کلید خصوصی شما هرگز از مرورگرتان خارج نمی‌شود.", + "certificateSection": "گواهی", + "uploadCert": "بارگذاری گواهی (‎.pfx، ‎.p12، ‎.pem)", + "certPassword": "گذرواژهٔ گواهی", + "privateKeyPassword": "گذرواژهٔ کلید خصوصی", + "certPasswordPlaceholder": "گذرواژهٔ گواهی را وارد کنید", + "certInfo": "اطلاعات گواهی", + "certSubject": "موضوع", + "certIssuer": "صادرکننده", + "certValidity": "معتبر", + "signatureDetails": "جزئیات امضا (اختیاری)", + "reason": "دلیل", + "reasonPlaceholder": "مثلاً: این سند را تأیید می‌کنم", + "location": "مکان", + "locationPlaceholder": "مثلاً: تهران، ایران", + "contactInfo": "اطلاعات تماس", + "contactPlaceholder": "مثلاً: email@example.com", + "visibleSignature": "امضای نمایان", + "visibleSignatureDescription": "با فعال‌سازی این گزینه، کادر امضای نمایانی روی PDF ظاهر می‌شود. می‌توانید تصویر یا نشان بارگذاری کنید یا متن بیفزایید. اگر هیچ‌کدام ارائه نشود، نام امضاکننده از روی گواهی به‌طور خودکار نمایش داده می‌شود.", + "signatureImage": "تصویر/نشان امضا (اختیاری)", + "uploadSignatureImage": "بارگذاری تصویر (PNG، JPG، WebP)", + "signaturePreviewAlt": "پیش‌نمایش امضا", + "remove": "برداشتن", + "page": "صفحه", + "firstPage": "صفحهٔ نخست", + "lastPage": "صفحهٔ پایانی", + "allPages": "همهٔ صفحات", + "customPage": "شمارهٔ صفحهٔ دلخواه", + "pageNumber": "شمارهٔ صفحه", + "xPosition": "موقعیت X (از چپ)", + "yPosition": "موقعیت Y (از بالا)", + "width": "عرض", + "height": "ارتفاع", + "addTextOverlay": "افزودن متن روی امضا", + "text": "متن", + "textPlaceholder": "مثلاً: به‌صورت دیجیتال امضا شده توسط John Doe", + "textColor": "رنگ متن", + "fontSize": "اندازهٔ قلم", + "invalidImageTitle": "تصویر نامعتبر", + "invalidImageMessage": "لطفاً یک تصویر PNG، JPG یا WebP برگزینید.", + "invalidFileTitle": "فایل نامعتبر", + "invalidFileMessage": "لطفاً یک فایل PDF برگزینید.", + "invalidCertificateTitle": "گواهی نامعتبر", + "invalidCertificateMessage": "لطفاً یک فایل گواهی ‎.pfx، ‎.p12 یا ‎.pem برگزینید.", + "certificateLoaded": "گواهی بارگذاری شد", + "certificateUnlocked": "گواهی باز شد", + "failedParsePem": "تجزیهٔ فایل PEM ناموفق بود", + "enterPasswordToUnlock": "برای باز کردن، گذرواژه را وارد کنید", + "invalidPasswordOrCertificate": "گذرواژه یا گواهی نامعتبر است", + "incorrectPassword": "گذرواژه نادرست است", + "failedParseCertificate": "تجزیهٔ گواهی ناموفق بود", + "missingDataTitle": "داده‌های ناقص", + "missingDataMessage": "لطفاً هم یک PDF و هم یک گواهی معتبر بارگذاری کنید.", + "applyingSignature": "در حال اعمال امضای دیجیتال…", + "signingFailedTitle": "امضا ناموفق بود", + "chainFetchFailed": "دریافت زنجیرهٔ گواهی ناموفق بود. این ممکن است به دلیل مشکلات شبکه یا در دسترس نبودن پراکسی گواهی باشد. لطفاً اتصال اینترنت خود را بررسی کرده و دوباره تلاش کنید. اگر مشکل ادامه یافت، با پشتیبانی تماس بگیرید.", + "failedToSignPdf": "امضای PDF ناموفق بود: {{message}}", + "defaultSignatureText": "به‌صورت دیجیتال امضا شده توسط {{signer}}\n{{date}}", + "pageCount": "{{count}} صفحه", + "howToStep1Title": "بارگذاری PDF", + "howToStep1Text": "سند PDF‌ای را که می‌خواهید امضا کنید برگزینید", + "howToStep2Title": "بارگذاری گواهی", + "howToStep2Text": "فایل گواهی ‎.pfx یا ‎.p12 و گذرواژهٔ آن را وارد کنید", + "howToStep3Title": "امضا و دانلود", + "howToStep3Text": "امضای دیجیتال را اعمال کرده و PDF امضاشدهٔ خود را دانلود کنید", + "relatedEncryptDesc": "PDF خود را با گذرواژه محافظت کنید", + "relatedDecryptDesc": "گذرواژهٔ PDF را بردارید", + "relatedFlattenDesc": "حاشیه‌نویسی‌ها را دائمی کنید", + "relatedSanitizeDesc": "داده‌های پنهان را حذف کنید", + "relatedPermissionsDesc": "مجوزهای PDF را تعیین کنید", + "faqWhatIsQuestion": "امضای دیجیتال چیست؟", + "faqWhatIsAnswer": "امضای دیجیتال سازوکاری رمزنگاری‌شده است که اصالت و یکپارچگی یک سند را تأیید می‌کند. برخلاف امضای دستی، این امضا از رمزنگاری کلید عمومی (گواهی‌های X.509) برای اثبات هویت امضاکننده بهره می‌گیرد.", + "faqFormatsQuestion": "از چه قالب‌های گواهی پشتیبانی می‌شود؟", + "faqFormatsAnswer": "ما از گواهی‌های قالب PKCS#12 (‎.pfx، ‎.p12) پشتیبانی می‌کنیم که هم گواهی و هم کلید خصوصی شما را در یک فایل رمزگذاری‌شده در بر دارند. این گواهی‌ها معمولاً توسط مراجع صدور گواهی صادر می‌شوند.", + "faqPrivateKeyQuestion": "آیا کلید خصوصی من ایمن است؟", + "faqPrivateKeyAnswer": "بله! تمام پردازش‌ها به‌طور کامل در مرورگر شما انجام می‌شود. گواهی، کلید خصوصی و فایل‌های PDF شما هرگز از دستگاهتان خارج نشده و روی هیچ سروری بارگذاری نمی‌شوند.", + "faqValidReadersQuestion": "آیا امضا در خوانندگان PDF معتبر خواهد بود؟", + "faqValidReadersAnswer": "بله، این ابزار امضاهای جداشدهٔ استاندارد PKCS#7 می‌سازد که همهٔ نمایشگرهای اصلی PDF آن‌ها را می‌شناسند. برای اعتبارسنجی کامل، از گواهی یک مرجع صدور گواهی مورد اعتماد استفاده کنید.", + "applySignature": "اعمال امضای دیجیتال", + "successMessage": "PDF با موفقیت امضا شد! این امضا در هر خوانندهٔ PDF قابل بررسی است.", + "internetRequiredTitle": "اتصال اینترنت لازم است", + "internetRequiredDescription": "امضا کردن ممکن است نیازمند دریافت اطلاعات زنجیرهٔ گواهی از سرور صادرکنندهٔ گواهی شما برای اعتبارسنجی امضا باشد." + }, + "validateSignaturePdf": { + "name": "اعتبارسنجی امضای PDF", + "pageTitle": "اعتبارسنجی امضای PDF - بررسی امضاهای دیجیتال | BentoPDF", + "subtitle": "امضاهای دیجیتال فایل‌های PDF خود را بررسی کنید. اعتبار گواهی را وارسی کنید، جزئیات امضاکننده را ببینید و یکپارچگی سند را تأیید کنید. تمام پردازش‌ها در مرورگر شما انجام می‌شود." + }, + "timestampPdf": { + "name": "مُهر زمانی PDF", + "pageTitle": "مُهر زمانی PDF آنلاین و رایگان - مُهر زمانی سند بر پایهٔ RFC 3161 | BentoPDF", + "subtitle": "با استفاده از یک سرور مرجع مُهر زمانی (TSA) مورد اعتماد، یک مُهر زمانی سند بر پایهٔ RFC 3161 به PDF خود بیفزایید. این کار اثبات می‌کند سند شما در زمانی مشخص وجود داشته است. نیازی به گواهی نیست.", + "tsaSectionTitle": "سرور مُهر زمانی (TSA)", + "selectTsa": "یک سرور TSA برگزینید", + "applyTimestamp": "اعمال مُهر زمانی", + "successMessage": "مُهر زمانی با موفقیت به PDF افزوده شد! این مُهر زمانی در هر خوانندهٔ PDF قابل بررسی است." + }, + "emailToPdf": { + "name": "ایمیل به PDF", + "subtitle": "فایل‌های ایمیل (EML، MSG) را به قالب PDF تبدیل کنید. از برون‌ریزی‌های Outlook و قالب‌های استاندارد ایمیل پشتیبانی می‌شود.", + "acceptedFormats": "فایل‌های EML، MSG", + "convertButton": "تبدیل به PDF" + }, + "fontToOutline": { + "name": "تبدیل قلم به خطوط برداری", + "subtitle": "همهٔ قلم‌ها را به خطوط برداری تبدیل کنید تا نمایش یکسانی در همهٔ دستگاه‌ها داشته باشند." + }, + "deskewPdf": { + "name": "صاف‌کردن PDF", + "subtitle": "صفحات اسکن‌شدهٔ کج را به‌طور خودکار و با OpenCV صاف کنید." + }, + "pdfToWord": { + "name": "PDF به Word", + "subtitle": "فایل‌های PDF را به اسناد Word قابل ویرایش تبدیل کنید." + }, + "extractImages": { + "name": "استخراج تصاویر", + "subtitle": "همهٔ تصاویر تعبیه‌شده در فایل‌های PDF خود را استخراج کنید." + }, + "pdfToMarkdown": { + "name": "PDF به Markdown", + "subtitle": "متن و جدول‌های PDF را به قالب Markdown تبدیل کنید." + }, + "preparePdfForAi": { + "name": "آماده‌سازی PDF برای هوش مصنوعی", + "subtitle": "محتوای PDF را به‌صورت JSON سازگار با LlamaIndex برای خط‌لوله‌های RAG/LLM استخراج کنید." + }, + "pdfOcg": { + "name": "لایه‌های OCG در PDF", + "subtitle": "لایه‌های OCG را در PDF خود مشاهده، فعال/غیرفعال، اضافه و حذف کنید." + }, + "pdfToPdfa": { + "name": "PDF به PDF/A", + "subtitle": "PDF را برای بایگانی بلندمدت به PDF/A تبدیل کنید." + }, + "rasterizePdf": { + "name": "رستر کردن PDF", + "subtitle": "PDF را به PDF مبتنی بر تصویر تبدیل کنید. لایه‌ها را مسطح کرده و متن قابل انتخاب را حذف کنید." + }, + "pdfWorkflow": { + "name": "سازندهٔ گردش کار PDF", + "subtitle": "با یک ویرایشگر گرهی دیداری، خط‌لوله‌های پردازش PDF سفارشی بسازید.", + "nodes": "گره‌ها", + "searchNodes": "جستجوی گره‌ها…", + "run": "اجرا", + "clear": "پاک‌سازی", + "save": "ذخیره", + "load": "بارگذاری", + "export": "برون‌ریزی", + "import": "درون‌ریزی", + "ready": "آماده", + "settings": "تنظیمات", + "processing": "در حال پردازش…", + "saveTemplate": "ذخیرهٔ قالب", + "templateName": "نام قالب", + "templatePlaceholder": "مثلاً گردش کار فاکتور", + "cancel": "انصراف", + "loadTemplate": "بارگذاری قالب", + "noTemplates": "هنوز هیچ قالب ذخیره‌شده‌ای وجود ندارد.", + "ok": "باشه", + "workflowCompleted": "گردش کار به پایان رسید", + "errorDuringExecution": "خطا هنگام اجرا", + "addNodeError": "برای اجرای گردش کار، دست‌کم یک گره بیفزایید.", + "needInputOutput": "گردش کار شما برای اجرا به دست‌کم یک گره ورودی و یک گره خروجی نیاز دارد.", + "enterName": "لطفاً یک نام وارد کنید.", + "templateExists": "قالبی با این نام از پیش وجود دارد.", + "templateSaved": "قالب «{{name}}» ذخیره شد.", + "templateLoaded": "قالب «{{name}}» بارگذاری شد.", + "failedLoadTemplate": "بارگذاری قالب ناموفق بود.", + "noSettings": "این گره تنظیمات قابل پیکربندی ندارد.", + "advancedSettings": "تنظیمات پیشرفته", + "specialNodes": { + "pdfInput": { + "name": "ورودی PDF", + "description": "یک یا چند فایل PDF بارگذاری کنید" + }, + "download": { + "name": "دانلود", + "description": "دانلود خودکار به‌صورت PDF یا ZIP" + } + }, + "errors": { + "noPdfsConnected": "هیچ PDF‌ای به گرهٔ {{node}} متصل نشده است", + "noPdfInputs": "هیچ PDF‌ای در ورودی وجود ندارد", + "expectedPdfInput": "ورودی PDF انتظار می‌رفت", + "failedToRenderPage": "ترسیم صفحهٔ {{page}} ناموفق بود", + "failedToGetCanvasContext": "دریافت زمینهٔ canvas برای صفحهٔ {{page}} ناموفق بود", + "failedToRenderPageToImage": "ترسیم صفحهٔ {{page}} به تصویر ناموفق بود", + "noPagesProcessed": "هیچ صفحه‌ای پردازش نشد", + "cpdfNotConfigured": "CoherentPDF پیکربندی نشده است. لطفاً آن را در تنظیمات پیشرفته پیکربندی کنید.", + "workerError": "خطای worker: {{message}}", + "alternateMergeWorkerError": "خطای worker ادغام یک‌درمیان: {{message}}", + "noPdfFilesUploaded": "هیچ فایل PDF‌ای در گرهٔ ورودی PDF بارگذاری نشده است", + "failedToLoadFile": "بارگذاری «{{file}}» ناموفق بود - ممکن است فایل آسیب‌دیده باشد", + "noImagesUploaded": "هیچ تصویری در گرهٔ ورودی تصویر بارگذاری نشده است", + "noWordDocsUploaded": "هیچ سندی در گرهٔ ورودی Word بارگذاری نشده است", + "noExcelUploaded": "هیچ صفحه‌گسترده‌ای در گرهٔ ورودی Excel بارگذاری نشده است", + "noPowerPointUploaded": "هیچ ارائه‌ای در گرهٔ ورودی PowerPoint بارگذاری نشده است", + "noCbzUploaded": "هیچ بایگانی کمیکی در گرهٔ ورودی CBZ بارگذاری نشده است", + "noEmailUploaded": "هیچ فایل ایمیلی در گرهٔ ورودی ایمیل بارگذاری نشده است", + "noEpubUploaded": "هیچ فایل EPUB‌ای در گرهٔ ورودی EPUB بارگذاری نشده است", + "noFb2Uploaded": "هیچ فایل FB2‌ای در گرهٔ ورودی FB2 بارگذاری نشده است", + "noJsonUploaded": "هیچ فایل JSON‌ای در گرهٔ ورودی JSON بارگذاری نشده است", + "noMarkdownUploaded": "هیچ فایل Markdown‌ای در گرهٔ ورودی Markdown بارگذاری نشده است", + "noMobiUploaded": "هیچ فایل MOBI‌ای در گرهٔ ورودی MOBI بارگذاری نشده است", + "noOdgUploaded": "هیچ فایل ODG‌ای در گرهٔ ورودی ODG بارگذاری نشده است", + "noPagesUploaded": "هیچ فایل Pages‌ای در گرهٔ ورودی Pages بارگذاری نشده است", + "noPubUploaded": "هیچ فایل PUB‌ای در گرهٔ ورودی PUB بارگذاری نشده است", + "noSvgUploaded": "هیچ فایل SVG‌ای در گرهٔ ورودی SVG بارگذاری نشده است", + "noTextUploaded": "هیچ فایل متنی‌ای در گرهٔ ورودی متن بارگذاری نشده است", + "noVsdUploaded": "هیچ فایل Visio‌ای در گرهٔ ورودی VSD بارگذاری نشده است", + "noWpdUploaded": "هیچ فایل WPD‌ای در گرهٔ ورودی WPD بارگذاری نشده است", + "noWpsUploaded": "هیچ فایل WPS‌ای در گرهٔ ورودی WPS بارگذاری نشده است", + "noXmlUploaded": "هیچ فایل XML‌ای در گرهٔ ورودی XML بارگذاری نشده است", + "noXpsUploaded": "هیچ فایل XPS‌ای در گرهٔ ورودی XPS بارگذاری نشده است", + "alternateMergeNeedsTwo": "ادغام یک‌درمیان به دست‌کم دو PDF متصل به ورودی خود نیاز دارد", + "cropMarginsExceed": "حاشیه‌های برش از ابعاد صفحه فراتر می‌روند. مقادیر برش را کاهش دهید.", + "digitalSignNoCertificate": "هیچ گواهی‌ای در گرهٔ امضای دیجیتال بارگذاری نشده است", + "encryptUserPasswordRequired": "برای رمزگذاری، گذرواژهٔ کاربر لازم است", + "extractImagesNoneFound": "در هیچ‌یک از PDFهای متصل تصویری یافت نشد", + "invertColorsFailedReadImage": "خواندن داده‌های تصویر ناموفق بود", + "overlayRequiresPdf": "گرهٔ روی‌گذاری به یک PDF ورودی برای روی‌گذاری نیاز دارد.", + "pdfToCsvNoTables": "هیچ جدولی در PDF یافت نشد", + "pdfToXlsxNoTables": "هیچ جدولی در {{file}} یافت نشد", + "posterizeFailedTileCanvas": "دریافت زمینهٔ canvas کاشی ناموفق بود", + "redactNoText": "سانسور: هیچ متنی برای سانسور مشخص نشده است", + "removeBlankPagesAllBlank": "همهٔ صفحات خالی هستند", + "svgFailedToConvert": "تبدیل SVG ناموفق بود", + "svgFailedToLoad": "بارگذاری SVG ناموفق بود", + "timestampFailed": "افزودن مُهر زمانی با TSA {{tsa}} ناموفق بود: {{message}}", + "noOutputNodes": "هیچ گرهٔ خروجی یافت نشد. برای تکمیل گردش کار خود، یک گرهٔ دانلود بیفزایید.", + "circularDependency": "وابستگی حلقوی در گردش کار تشخیص داده شد. لطفاً حلقه‌های میان گره‌ها را بردارید.", + "encryptOrdering": "گرهٔ رمزگذاری به «{{target}}» خورانده می‌شود که ممکن است روی داده‌های رمزگذاری‌شده شکست بخورد. گرهٔ رمزگذاری را درست پیش از گرهٔ خروجی قرار دهید.", + "invalidWorkflowFile": "فایل گردش کار نامعتبر است: آرایهٔ گره‌ها یا اتصال‌ها وجود ندارد.", + "storageQuotaExceeded": "ذخیرهٔ گردش کار ناموفق بود: سهمیهٔ فضای ذخیره‌سازی پر شده است. قالب‌های قدیمی را حذف کنید.", + "failedToImport": "درون‌ریزی گردش کار ناموفق بود: {{message}}" + } + }, + "wasmSettings": { + "title": "تنظیمات قابلیت‌های پیشرفته", + "description": "برای فعال‌سازی قابلیت‌های پیشرفتهٔ PDF، ماژول‌های پردازش خارجی را پیکربندی کنید. این ماژول‌ها اختیاری بوده و مجوز جداگانه دارند.", + "status": { + "testing": "در حال آزمایش…", + "customOverride": "بازنویسی سفارشی", + "preConfigured": "از پیش پیکربندی‌شده", + "notConfigured": "پیکربندی نشده" + }, + "preconfiguredTitle": "از پیش پیکربندی‌شده و آمادهٔ استفاده.", + "preconfiguredDescription": "ماژول‌های پردازش پیشرفته به‌طور خودکار از CDN بارگذاری می‌شوند. اگر نیاز به منبع سفارشی دارید (مثلاً برای استقرارهای جدا از شبکه یا میزبانی‌شخصی)، می‌توانید نشانی‌های زیر را بازنویسی کنید.", + "usingDefault": "استفاده از پیش‌فرض: {{url}}", + "pymupdfSubtitle": "موتور پردازش سند", + "pymupdfFeatures": "فعال می‌کند: PDF به متن، Markdown، SVG، DOCX، Excel • استخراج تصاویر/جدول‌ها • تبدیل قالب", + "testButton": "آزمایش", + "recommendedLabel": "پیشنهادی:", + "copyTitle": "کپی در حافظهٔ موقت", + "ghostscriptSubtitle": "موتور تبدیل PDF/A", + "ghostscriptFeatures": "فعال می‌کند: تبدیل PDF/A-1b، PDF/A-2b، PDF/A-3b • تبدیل قلم به خطوط برداری", + "cpdfSubtitle": "موتور نشانک‌ها و فراداده", + "cpdfFeatures": "فعال می‌کند: تقسیم بر پایهٔ نشانک • ویرایش نشانک‌ها • فرادادهٔ PDF", + "saveConfiguration": "ذخیرهٔ پیکربندی", + "savingConfiguration": "در حال ذخیرهٔ پیکربندی…", + "savedTitle": "ذخیره شد", + "savedMessage": "پیکربندی با موفقیت ذخیره شد!", + "errorTitle": "خطا", + "failedSave": "ذخیرهٔ پیکربندی ناموفق بود: {{message}}", + "emptyUrlTitle": "نشانی خالی", + "emptyUrlMessage": "لطفاً برای آزمایش یک نشانی وارد کنید.", + "testSuccessTitle": "موفق", + "connectionSuccess": "اتصال به {{package}} موفق بود!", + "connectionFailedTitle": "اتصال ناموفق", + "connectionFailedMessage": "اتصال به این نشانی ممکن نشد.", + "resetDefaults": "بازنشانی به پیش‌فرض‌ها", + "resetTitle": "بازنشانی", + "resetWithDefaults": "بازنویسی‌های سفارشی پاک شدند. پیش‌فرض‌های از پیش پیکربندی‌شده فعال هستند.", + "resetNoDefaults": "همهٔ پیکربندی‌ها و ماژول‌های ذخیره‌شده در حافظهٔ نهان پاک شدند.", + "licenseTitle": "اطلاعیهٔ مجوز:", + "licenseDescription": "ماژول‌های خارجی (PyMuPDF، Ghostscript، CoherentPDF) تحت مجوز AGPL-3.0 یا مجوزهای کپی‌لفت مشابه ارائه می‌شوند. با پیکربندی و استفاده از این ماژول‌ها، شما با شرایط مجوز مربوط به هر یک موافقت می‌کنید. BentoPDF با هر پیاده‌سازی Ghostscript WASM و PyMuPDF WASM که از رابط مورد انتظار پیروی کند سازگار است." + }, + "pdfOverlay": { + "name": "روی‌گذاری PDF", + "subtitle": "صفحات یک PDF را روی یا زیر PDF دیگری قرار دهید.", + "basePdfLabel": "PDF پایه", + "uploadBasePdf": "بارگذاری PDF پایه", + "overlayPdfLabel": "PDF روی‌گذاری / زیرگذاری", + "uploadOverlayPdf": "بارگذاری PDF روی‌گذاری/زیرگذاری", + "modeLabel": "حالت", + "overlayOption": "روی‌گذاری (بالای صفحات)", + "underlayOption": "زیرگذاری (پشت صفحات)", + "pageRangeLabel": "اعمال بر صفحات (اختیاری)", + "pageRangeHint": "برای صفحهٔ پایانی از «z» استفاده کنید. برای اعمال بر همهٔ صفحات، خالی بگذارید.", + "repeatLabel": "تکرار صفحات روی‌گذاری/زیرگذاری در صورت بلندتر بودن PDF پایه", + "processButton": "اعمال روی‌گذاری / زیرگذاری", + "howItWorksUploadTitle": "دو PDF بارگذاری کنید", + "howItWorksUploadDescription": "PDF پایه و PDF‌ای را که می‌خواهید روی یا زیر آن قرار دهید بارگذاری کنید.", + "howItWorksModeTitle": "انتخاب حالت", + "howItWorksModeDescription": "روی‌گذاری (بالا) یا زیرگذاری (پشت) را برگزینید. در صورت تمایل، بازهٔ صفحات و تکرار را تعیین کنید.", + "howItWorksDownloadTitle": "دانلود", + "howItWorksDownloadDescription": "PDF خود را با روی‌گذاری یا زیرگذاری اعمال‌شده دانلود کنید.", + "faqDifferenceQuestion": "تفاوت روی‌گذاری و زیرگذاری چیست؟", + "faqDifferenceAnswer": "روی‌گذاری صفحات را بالای PDF پایهٔ شما قرار می‌دهد (مانند واترمارک). زیرگذاری صفحات را پشت PDF پایهٔ شما قرار می‌دهد (مانند سربرگ یا پس‌زمینه).", + "faqSinglePageQuestion": "آیا می‌توانم از یک PDF تک‌صفحه‌ای به‌عنوان روی‌گذاری برای همهٔ صفحات استفاده کنم؟", + "faqSinglePageAnswer": "بله! گزینهٔ «تکرار» را فعال کنید تا صفحات PDF روی‌گذاری در همهٔ صفحات پایه تکرار شوند. یک واترمارک تک‌صفحه‌ای بر همهٔ صفحات اعمال خواهد شد.", + "faqPrivacyQuestion": "آیا فایل‌های من خصوصی و ایمن هستند؟", + "faqPrivacyAnswer": "کاملاً! تمام پردازش‌ها در مرورگر شما انجام می‌شود. فایل‌های شما هرگز از دستگاهتان خارج نمی‌شوند و حریم خصوصی کامل تضمین می‌شود." + } +} diff --git a/scripts/generate-i18n-pages.mjs b/scripts/generate-i18n-pages.mjs index a952f6b9..be0c2bf5 100644 --- a/scripts/generate-i18n-pages.mjs +++ b/scripts/generate-i18n-pages.mjs @@ -183,7 +183,7 @@ function processFileForLanguage( const document = dom.window.document; document.documentElement.lang = lang; - document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr'; + document.documentElement.dir = ['ar', 'fa'].includes(lang) ? 'rtl' : 'ltr'; let title = null; let description = null; diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index e0c80f70..30403050 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -24,6 +24,7 @@ export const supportedLanguages = [ 'ja', 'uk', 'sk', + 'fa', ] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; @@ -49,6 +50,7 @@ export const languageNames: Record = { ja: '日本語', uk: 'Українська', sk: 'Slovenčina', + fa: 'فارسی', }; export const getLanguageFromUrl = (): SupportedLanguage => { @@ -64,7 +66,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => { } const langMatch = path.match( - /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(?:\/|$)/ + /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|fa)(?:\/|$)/ ); if ( langMatch && @@ -154,7 +156,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => { let pagePathWithoutLang = relativePath; const langPrefixMatch = relativePath.match( - /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(\/.*)?$/ + /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|fa)(\/.*)?$/ ); if (langPrefixMatch) { pagePathWithoutLang = langPrefixMatch[2] || '/'; @@ -217,7 +219,9 @@ export const applyTranslations = (): void => { }); document.documentElement.lang = i18next.language; - document.documentElement.dir = i18next.language === 'ar' ? 'rtl' : 'ltr'; + document.documentElement.dir = ['ar', 'fa'].includes(i18next.language) + ? 'rtl' + : 'ltr'; }; export const rewriteLinks = (): void => { @@ -249,7 +253,7 @@ export const rewriteLinks = (): void => { } const langPrefixRegex = new RegExp( - `^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(/|$)` + `^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|fa)(/|$)` ); if (langPrefixRegex.test(href)) { return; diff --git a/vite.config.ts b/vite.config.ts index 9bfe50a7..60acc486 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,6 +35,7 @@ const SUPPORTED_LANGUAGES = [ 'ja', 'uk', 'sk', + 'fa', ] as const; const LANG_REGEX = new RegExp( `^/(${SUPPORTED_LANGUAGES.join('|')})(?:/(.*))?$` From 612654e7d3e62e273f7f348f113fb1a08b81abcb Mon Sep 17 00:00:00 2001 From: Kouros Zanbouri Date: Fri, 31 Jul 2026 13:17:06 +0100 Subject: [PATCH 02/10] Fix BentoPDF casing in Farsi locale Use canonical BentoPDF instead of Bentopdf in about.philosophy.description and about.cta.description, per CodeRabbit review. --- public/locales/fa/common.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index e0038eea..f7697cde 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -292,7 +292,7 @@ "philosophy": { "label": "فلسفهٔ بنیادین ما", "title": "نخست حریم خصوصی. همیشه.", - "description": "در روزگاری که داده به کالا تبدیل شده است، ما راه دیگری در پیش گرفته‌ایم. تمام پردازش ابزارهای Bentopdf به‌صورت محلی در مرورگر شما انجام می‌شود. این یعنی فایل‌های شما هرگز به سرورهای ما نمی‌رسند، ما هرگز اسنادتان را نمی‌بینیم و کارهایتان را ردیابی نمی‌کنیم. اسناد شما به‌طور کامل و بی‌چون‌وچرا خصوصی می‌مانند. این تنها یک قابلیت نیست؛ بنیان کار ماست." + "description": "در روزگاری که داده به کالا تبدیل شده است، ما راه دیگری در پیش گرفته‌ایم. تمام پردازش ابزارهای BentoPDF به‌صورت محلی در مرورگر شما انجام می‌شود. این یعنی فایل‌های شما هرگز به سرورهای ما نمی‌رسند، ما هرگز اسنادتان را نمی‌بینیم و کارهایتان را ردیابی نمی‌کنیم. اسناد شما به‌طور کامل و بی‌چون‌وچرا خصوصی می‌مانند. این تنها یک قابلیت نیست؛ بنیان کار ماست." }, "whyBentopdf": { "title": "چرا", @@ -315,7 +315,7 @@ }, "cta": { "title": "آمادهٔ شروع هستید؟", - "description": "به هزاران کاربری بپیوندید که برای نیازهای روزمرهٔ اسنادشان به Bentopdf اعتماد می‌کنند. تفاوتی را که حریم خصوصی و کارایی رقم می‌زند، تجربه کنید.", + "description": "به هزاران کاربری بپیوندید که برای نیازهای روزمرهٔ اسنادشان به BentoPDF اعتماد می‌کنند. تفاوتی را که حریم خصوصی و کارایی رقم می‌زند، تجربه کنید.", "button": "کاوش در همهٔ ابزارها" } }, From 89bde584ec2c462d37d07a7489b582820eada448 Mon Sep 17 00:00:00 2001 From: Kouros Zanbouri Date: Sun, 30 Aug 2026 22:51:14 +0100 Subject: [PATCH 03/10] Sync Farsi translation with upstream and list it in the docs Translate the 159 tools.json keys added upstream after this branch was opened: - editPdfText (114) - the PDF text editor - duplexCollate (43) - the duplex collate tool - mergePdf.removeDuplicateFonts - pdfToTiff.alert.conversionOutOfMemory fa/tools.json is now at full parity with en/tools.json (1039 keys). Also register Farsi in the README language table and in the TRANSLATION.md supported-language list and coverage table, matching what the Korean and Slovak additions did. --- README.md | 1 + TRANSLATION.md | 2 + public/locales/fa/tools.json | 167 ++++++++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d24cf108..4909ea29 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,7 @@ BentoPDF is available in multiple languages: | Vietnamese | [![Vietnamese](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/vi/common.json) | | Korean | [![Korean](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/ko/common.json) | | Russian | [![Russian](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/ru/common.json) | +| Persian (Farsi) | [![Persian](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/fa/common.json) | Want to help translate BentoPDF into your language? Check out our [Translation Guide](TRANSLATION.md)! diff --git a/TRANSLATION.md b/TRANSLATION.md index 579a5ea2..5a8599c5 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -40,6 +40,7 @@ BentoPDF uses **i18next** for internationalization (i18n). Currently supported l - **Danish** (`da`) - **Swedish** (`sv`) - **Japanese** (`ja`) +- **Persian (Farsi)** (`fa`) The app automatically detects the language from the URL path: @@ -617,6 +618,7 @@ Current translation coverage: | Danish | `da` | ✅ Complete | Community | | Swedish | `sv` | ✅ Complete | Community | | Japanese | `ja` | ✅ Complete | Community | +| Persian (Farsi) | `fa` | ✅ Complete | Community | | Your Language | `??` | 🚧 In Progress | You? | --- diff --git a/public/locales/fa/tools.json b/public/locales/fa/tools.json index 6496e516..9bd4e85f 100644 --- a/public/locales/fa/tools.json +++ b/public/locales/fa/tools.json @@ -19,7 +19,8 @@ "name": "ادغام PDF", "subtitle": "چند PDF را در یک فایل ترکیب کنید. نشانک‌ها حفظ می‌شوند.", "retainPageLabels": "حفظ برچسب‌های اصلی صفحات هر فایل", - "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید)." + "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید).", + "removeDuplicateFonts": "حذف قلم‌های تکراری" }, "splitPdf": { "name": "تقسیم PDF", @@ -55,6 +56,122 @@ "name": "ویرایشگر PDF", "subtitle": "حاشیه‌نویسی، هایلایت، سانسور، درج نظر، افزودن شکل و تصویر، جستجو و مشاهدهٔ PDF." }, + "editPdfText": { + "name": "ویرایش متن PDF", + "subtitle": "روی هر پاراگراف کلیک کنید و آن را در جای خود، همراه با بازچینش زنده، قلم‌ها و قالب‌بندی، ویرایش کنید.", + "saved": "PDF با موفقیت ذخیره شد! ({{size}} کیلوبایت)", + "failedToLoad": "بارگذاری ویرایشگر متن PDF ناموفق بود.", + "howToUpload": "بارگذاری PDF", + "howToClick": "کلیک روی یک پاراگراف", + "howToRestyle": "تغییر قالب هر عنصر", + "relatedPdfEditor": "حاشیه‌نویسی و علامت‌گذاری PDF", + "relatedSignPdf": "امضا را بکشید، تایپ کنید یا بارگذاری کنید", + "relatedWatermark": "درج مهر متنی یا تصویری", + "relatedStamps": "قرار دادن مهر روی صفحات", + "relatedCompress": "کاهش حجم فایل", + "relatedOrganizeName": "سازماندهی PDF", + "relatedOrganize": "مرتب‌سازی و مدیریت صفحات", + "open": "باز کردن", + "export": "برون‌ریزی", + "filterAll": "همه", + "filterText": "متن", + "filterImages": "تصاویر", + "filterShapes": "شکل‌ها", + "addText": "افزودن متن", + "langAuto": "خودکار", + "change": "تغییر", + "ignore": "نادیده گرفتن", + "next": "بعدی", + "replace": "جایگزینی", + "replaceAll": "جایگزینی همه", + "emptyState": "یک PDF باز کنید تا متن، تصاویر و شکل‌های آن را ویرایش کنید.", + "choosePdf": "یک PDF انتخاب کنید…", + "font": "قلم", + "systemFonts": "قلم‌های سیستم", + "color": "رنگ", + "alignment": "ترازبندی", + "style": "سبک", + "lists": "فهرست‌ها", + "outline": "دورخط", + "spacing": "فاصله‌گذاری", + "direction": "جهت", + "dirAuto": "خودکار", + "pathStroke": "خط مسیر", + "tipBackToUpload": "بازگشت به بارگذاری", + "tipOpenAnotherPdf": "باز کردن PDF دیگر", + "tipExportS": "برون‌ریزی (⌘S)", + "tipUndoZ": "واگرد (⌘Z)", + "tipRedoZ": "ازنو (⇧⌘Z)", + "tipFileDetails": "جزئیات فایل", + "tipEditExistingContent": "ویرایش محتوای موجود", + "tipWhatTheEditToolTouchesNarrowTheScopeOnBus": "آنچه ابزار ویرایش تغییر می‌دهد — دامنه را در صفحات شلوغ محدود کنید", + "tipClickThePageToAddATextBox": "برای افزودن کادر متن روی صفحه کلیک کنید", + "tipSpellCheckLanguage": "زبان غلط‌یاب", + "tipFindAndReplace": "یافتن و جایگزینی", + "tipAddAnImage": "افزودن تصویر", + "tipCopyThisPageSTextInReadingOrder": "رونوشت متن این صفحه به ترتیب خواندن", + "tipCheckTheSpellingOfThisPage": "بررسی املای این صفحه", + "tipShowOrHideTheFormatPanel": "نمایش یا پنهان کردن پنل قالب‌بندی", + "phReplaceWith": "جایگزینی با", + "tipAddToYourDictionary": "افزودن به واژه‌نامهٔ شما", + "tipClose": "بستن", + "phFind": "یافتن", + "tipPreviousMatch": "مورد قبلی", + "tipNextMatch": "مورد بعدی", + "tipRotateLeft90": "چرخش ۹۰ درجه به چپ", + "tipRotateRight90": "چرخش ۹۰ درجه به راست", + "tipFlipHorizontal": "وارونه‌سازی افقی", + "tipFlipVertical": "وارونه‌سازی عمودی", + "tipReplaceThisImageKeepingItsPlacement": "جایگزینی این تصویر با حفظ جایگاه آن", + "tipAlternateTextForAccessibilityTaggedPdfs": "متن جایگزین برای دسترس‌پذیری (PDFهای برچسب‌دار)", + "tipBringToFront": "انتقال به جلو", + "tipSendToBack": "انتقال به عقب", + "tipDuplicate": "تکثیر", + "tipDelete": "حذف", + "tipAlignLeftEdges": "تراز لبه‌های چپ", + "tipAlignHorizontalCenters": "تراز مراکز افقی", + "tipAlignRightEdges": "تراز لبه‌های راست", + "tipAlignTops": "تراز بالاها", + "tipAlignVerticalCenters": "تراز مراکز عمودی", + "tipAlignBottoms": "تراز پایین‌ها", + "tipDistributeHorizontally": "توزیع افقی", + "tipDistributeVertically": "توزیع عمودی", + "tipFontSize": "اندازهٔ قلم", + "tipListFontsInstalledOnThisDeviceChromium": "فهرست قلم‌های نصب‌شده روی این دستگاه (Chromium)", + "tipCustomTextColor": "رنگ متن دلخواه", + "tipAlignLeft": "تراز چپ", + "tipAlignCenter": "تراز وسط", + "tipAlignRight": "تراز راست", + "tipJustify": "تراز از دو طرف", + "tipBold": "توپر", + "tipItalic": "مورب", + "tipUnderline": "زیرخط", + "tipStrikethrough": "خط‌خورده", + "tipSuperscript": "بالانویس", + "tipSubscript": "پایین‌نویس", + "tipToggleBulletedItem": "تغییر وضعیت فهرست نشانه‌دار", + "tipToggleNumberedItem": "تغییر وضعیت فهرست شماره‌دار", + "tipNumberedListMarkerStyle": "سبک نشانهٔ فهرست شماره‌دار", + "tipDecreaseListIndentTab": "کاهش تورفتگی فهرست (⇧Tab)", + "tipIncreaseListIndentTab": "افزایش تورفتگی فهرست (Tab)", + "tipTextOutlineColor": "رنگ دورخط متن", + "tipOutlineWidth0None": "ضخامت دورخط (۰ = بدون دورخط)", + "tipFromContent": "بر پایهٔ محتوا", + "tipLeftToRight": "چپ به راست", + "tipRightToLeft": "راست به چپ", + "tipStrokeColor": "رنگ خط", + "tipStrokeWidth": "ضخامت خط", + "tipZoomPresets": "بزرگ‌نمایی‌های از پیش تعیین‌شده", + "tipZoomOut": "کوچک‌نمایی", + "tipZoomIn": "بزرگ‌نمایی", + "tipFitPage": "جا دادن صفحه", + "tipPreviousPage": "صفحهٔ قبلی", + "tipTypeAPageNumberAndPressEnter": "شمارهٔ صفحه را وارد کنید و Enter را بزنید", + "tipNextPage": "صفحهٔ بعدی", + "tipDocumentProperties": "ویژگی‌های سند", + "tipHideBar": "پنهان کردن نوار", + "tipShowBar": "نمایش نوار" + }, "jpgToPdf": { "name": "JPG به PDF", "subtitle": "از تصاویر JPG، JPEG و JPEG2000 (JP2/JPX) یک PDF بسازید." @@ -399,7 +516,8 @@ "noFile": "بدون فایل", "noFileExplanation": "لطفاً نخست یک فایل PDF بارگذاری کنید.", "conversionSuccess": "PDF با موفقیت به TIFF تبدیل شد!", - "conversionError": "تبدیل PDF به TIFF ناموفق بود. ممکن است فایل آسیب‌دیده باشد." + "conversionError": "تبدیل PDF به TIFF ناموفق بود. ممکن است فایل آسیب‌دیده باشد.", + "conversionOutOfMemory": "حافظهٔ مرورگر هنگام تبدیل تمام شد. DPI کمتری را امتحان کنید، یا گزینهٔ TIFF چندصفحه‌ای را بردارید تا صفحات یکی‌یکی تبدیل شوند." } }, "pdfToCbz": { @@ -514,6 +632,51 @@ "retainPageLabels": "حفظ برچسب‌های اصلی صفحات هر فایل", "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید)." }, + "duplexCollate": { + "name": "تلفیق دورو", + "subtitle": "PDF اسکن‌شده به‌صورت دو بلوک (ابتدا همهٔ روها، سپس همهٔ پشت‌ها) را به ترتیب درست رو/پشت مرتب کنید.", + "totalPagesLabel": "کل صفحات:", + "splitAfterPageLabel": "تقسیم پس از صفحهٔ", + "splitAfterPagePlaceholder": "مثلاً ۵۰", + "backBlockOrderLabel": "ترتیب بلوک پشت", + "backBlockReverseOption": "معکوس کردن بلوک پشت (رایج برای دسته‌های برگردانده‌شده)", + "backBlockKeepOption": "حفظ بلوک پشت به همان صورت", + "autoSplitButton": "تشخیص خودکار نقطهٔ تقسیم (نیمه)", + "exportGroupedLabel": "برون‌ریزی فایل‌های گروه‌بندی‌شده بر پایهٔ طول سند اصلی", + "pagesPerDocumentLabel": "تعداد صفحات هر سند اصلی", + "groupedHelp": "در صورت فعال بودن، خروجی یک فایل ZIP شامل یک PDF برای هر سند گروه‌بندی‌شده است.", + "collateButton": "تلفیق PDF", + "invalidFileTitle": "فایل نامعتبر", + "invalidFileMessage": "لطفاً یک فایل PDF انتخاب کنید.", + "loadingPdf": "در حال بارگذاری PDF…", + "loadErrorMessage": "بارگذاری فایل PDF ناموفق بود.", + "fileMeta": "{{size}} - {{count}} صفحه", + "oddPageWarning": "تعداد صفحات فرد است ({{count}}). یک اسکن دورو باید تعداد صفحات زوج داشته باشد؛ ممکن است صفحه‌ای جا افتاده یا اضافه باشد.", + "previewSummary": "بلوک رو: {{frontCount}} صفحه، بلوک پشت: {{backCount}} صفحه. خروجی: {{outputCount}} صفحه، به ترتیب رو/پشت.", + "previewMismatchWarning": "طول بلوک‌های رو و پشت متفاوت است ({{frontCount}} رو در برابر {{backCount}} پشت). {{extraCount}} صفحهٔ اضافی در سمت بلندتر بدون جفت به انتها افزوده می‌شود.", + "missingFileTitle": "فایل موجود نیست", + "missingFileMessage": "لطفاً پیش از پردازش، یک PDF با دست‌کم ۲ صفحه بارگذاری کنید.", + "invalidSplitTitle": "نقطهٔ تقسیم نامعتبر", + "invalidSplitMessage": "نقطهٔ تقسیم باید بین صفحهٔ ۱ و صفحهٔ {{maxSplit}} باشد.", + "unevenConfirmTitle": "بلوک‌های نابرابر", + "unevenConfirmMessage": "بلوک رو {{frontCount}} صفحه و بلوک پشت {{backCount}} صفحه دارد.\nاین معمولاً یعنی صفحه‌ای دو بار اسکن شده یا جا افتاده است.\n\n{{extraCount}} صفحهٔ بدون جفت به انتها افزوده می‌شود.\n\nبه هر حال ادامه می‌دهید؟", + "invalidGroupTitle": "اندازهٔ گروه نامعتبر", + "invalidGroupMessage": "لطفاً تعداد معتبری برای صفحات هر سند اصلی وارد کنید.", + "collatingLoader": "در حال تلفیق اسکن دورو…", + "successMessage": "PDF تلفیق‌شده با موفقیت ساخته شد.", + "successGroupedMessage": "تلفیق کامل شد. {{fileCount}} فایل گروه‌بندی‌شده ساخته شد.", + "processErrorMessage": "تلفیق PDF ناموفق بود.", + "howItWorksStep1Title": "اسکن را بارگذاری کنید", + "howItWorksStep1": "همان یک PDF را که ابتدا همهٔ روها و سپس همهٔ پشت‌ها را دارد رها کنید.", + "howItWorksStep2Title": "نقطهٔ تقسیم را تعیین کنید", + "howItWorksStep2": "مشخص کنید بلوک رو کجا پایان می‌یابد، سپس انتخاب کنید که آیا بلوک پشت باید معکوس شود.", + "howItWorksStep3Title": "تلفیق و دانلود", + "howItWorksStep3": "یک PDF درهم‌آمیخته دریافت کنید، یا یک ZIP با فایلی جداگانه برای هر سند اصلی.", + "faqOddPagesQuestion": "اگر اسکن من تعداد صفحات فرد داشته باشد چه؟", + "faqOddPagesAnswer": "یک اسکن دورو باید تعداد صفحات زوج داشته باشد. اگر چنین نیست، احتمالاً صفحه‌ای دو بار اسکن شده یا جا افتاده است. ابزار پیش از پردازش به شما هشدار می‌دهد و صفحات بدون جفت به انتها افزوده می‌شوند تا چیزی از دست نرود.", + "faqBackOrderQuestion": "چرا باید بلوک پشت را به همان صورت نگه دارم؟", + "faqBackOrderAnswer": "این به نحوهٔ قرار دادن دسته در گذر دوم بستگی دارد. برگرداندن کل دسته، پشت‌ها را به ترتیب معکوس تولید می‌کند که حالت پیش‌فرض است. اگر اسکنر شما آن‌ها را به همان ترتیب روها بازگرداند، گزینهٔ حفظ بلوک به همان صورت را انتخاب کنید." + }, "addAttachments": { "name": "افزودن پیوست", "subtitle": "یک یا چند فایل را در PDF خود تعبیه کنید." From 5a6d6bc411c5f92fa66f8c20ad9a57d075ea205e Mon Sep 17 00:00:00 2001 From: sergitost10 <118278737+sergitost10@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:49:50 +0200 Subject: [PATCH 04/10] feat(i18n): add Catalan language support --- nginx.conf | 20 +- public/locales/ca/common.json | 390 ++++++++++ public/locales/ca/tools.json | 1327 +++++++++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 8 +- vite.config.ts | 1 + 5 files changed, 1733 insertions(+), 13 deletions(-) create mode 100644 public/locales/ca/common.json create mode 100644 public/locales/ca/tools.json diff --git a/nginx.conf b/nginx.conf index 6045853b..bb3bfe0d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -50,11 +50,11 @@ http { return 301 /$1; } - if ($request_uri ~ ^(/(?:ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/src/pages/([a-z0-9][a-z0-9-]*)\.html(\?.*)?$) { + if ($request_uri ~ ^(/(?:ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/src/pages/([a-z0-9][a-z0-9-]*)\.html(\?.*)?$) { return 301 $1/$2$3; } - if ($request_uri ~ ^(/(?:ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/change-text-color(?:\.html)?/?(\?.*)?$) { + if ($request_uri ~ ^(/(?:ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/change-text-color(?:\.html)?/?(\?.*)?$) { return 301 $1/text-color$2; } @@ -93,27 +93,27 @@ http { return 301 /docs/tools/protect-pdf$1; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/index\.html(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/index\.html(\?.*)?$) { return 301 $1/$2; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/pdf-to-docx(?:\.html)?/?(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/pdf-to-docx(?:\.html)?/?(\?.*)?$) { return 301 $1/pdf-to-word$2; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/decrypt-pdf(?:\.html)?/?(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/decrypt-pdf(?:\.html)?/?(\?.*)?$) { return 301 $1/unlock-pdf$2; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/encrypt-pdf(?:\.html)?/?(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/encrypt-pdf(?:\.html)?/?(\?.*)?$) { return 301 $1/protect-pdf$2; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))?/([a-z0-9][a-z0-9-]*)\.html(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))?/([a-z0-9][a-z0-9-]*)\.html(\?.*)?$) { return 301 $1/$2$3; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja))/([a-z0-9][a-z0-9-]*)/(\?.*)?$) { + if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))/([a-z0-9][a-z0-9-]*)/(\?.*)?$) { return 301 $1/$2$3; } - if ($request_uri ~ ^/((?!(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|docs|blog)/)[a-z0-9][a-z0-9-]*)/(\?.*)?$) { + if ($request_uri ~ ^/((?!(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca|docs|blog)/)[a-z0-9][a-z0-9-]*)/(\?.*)?$) { return 301 /$1$2; } @@ -121,7 +121,7 @@ http { return 404; } - location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja)(/.*)?$ { + location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca)(/.*)?$ { try_files $uri $uri/ $uri.html =404; expires 5m; } diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json new file mode 100644 index 00000000..b0a867e0 --- /dev/null +++ b/public/locales/ca/common.json @@ -0,0 +1,390 @@ +{ + "nav": { + "home": "Inici", + "about": "Quant a", + "contact": "Contacte", + "licensing": "Llicències", + "allTools": "Totes les eines", + "docs": "Documentació", + "openMainMenu": "Obre el menú principal", + "language": "Idioma", + "searchLanguage": "Cerca un idioma…", + "noLanguagesFound": "No s'ha trobat cap idioma" + }, + "donation": { + "message": "T'agrada BentoPDF? Ajuda'ns a mantenir-lo gratuït i de codi obert!", + "button": "Fes una donació" + }, + "hero": { + "title": "El", + "pdfToolkit": "conjunt d'eines PDF", + "builtForPrivacy": "pensat per protegir la privadesa", + "noSignups": "Sense registre", + "unlimitedUse": "Ús il·limitat", + "worksOffline": "Funciona sense connexió", + "startUsing": "Comença a utilitzar-lo" + }, + "usedBy": { + "title": "Utilitzat per empreses i persones que treballen a" + }, + "features": { + "title": "Per què triar", + "bentoPdf": "BentoPDF?", + "noSignup": { + "title": "Sense registre", + "description": "Comença immediatament, sense comptes ni correus electrònics." + }, + "noUploads": { + "title": "Sense càrregues de fitxers", + "description": "100 % al navegador: els fitxers no surten mai del dispositiu." + }, + "foreverFree": { + "title": "Gratuït per sempre", + "description": "Totes les eines, sense períodes de prova ni pagaments." + }, + "noLimits": { + "title": "Sense límits", + "description": "Utilitza'l tant com vulguis, sense límits ocults." + }, + "batchProcessing": { + "title": "Processament per lots", + "description": "Processa tants PDF com vulguis alhora." + }, + "lightningFast": { + "title": "Extremadament ràpid", + "description": "Processa PDF a l'instant, sense esperes ni retards." + } + }, + "tools": { + "title": "Comença amb les", + "toolsLabel": "Eines", + "subtitle": "Fes clic en una eina per obrir el selector de fitxers", + "searchPlaceholder": "Cerca una eina (p. ex., «dividir», «organitzar»...)", + "backToTools": "Torna a les eines", + "firstLoadNotice": "La primera càrrega pot trigar una mica mentre es descarrega el motor de conversió. Les següents seran instantànies." + }, + "upload": { + "clickToSelect": "Fes clic per seleccionar un fitxer", + "orDragAndDrop": "o arrossega'l i deixa'l anar", + "pdfOrImages": "PDF o imatges", + "filesNeverLeave": "Els teus fitxers no surten mai del dispositiu.", + "addMore": "Afegeix més fitxers", + "clearAll": "Neteja-ho tot", + "clearFiles": "Elimina els fitxers", + "hints": { + "singlePdf": "Un únic fitxer PDF", + "pdfFile": "Fitxer PDF", + "multiplePdfs2": "Diversos fitxers PDF (com a mínim 2)", + "bmpImages": "Imatges BMP", + "oneOrMorePdfs": "Un o més fitxers PDF", + "pdfDocuments": "Documents PDF", + "oneOrMoreCsv": "Un o més fitxers CSV", + "multiplePdfsSupported": "S'admeten diversos fitxers PDF", + "singleOrMultiplePdfs": "S'admeten un o diversos fitxers PDF", + "singlePdfFile": "Un únic fitxer PDF", + "pdfWithForms": "Fitxer PDF amb camps de formulari", + "heicImages": "Imatges HEIC/HEIF", + "jpgImages": "Imatges JPG, JPEG, JP2 i JPX", + "pdfsOrImages": "PDF o imatges", + "oneOrMoreOdt": "Un o més fitxers ODT", + "singlePdfOnly": "Només un fitxer PDF", + "pdfFiles": "Fitxers PDF", + "multiplePdfs": "Diversos fitxers PDF", + "pngImages": "Imatges PNG", + "pdfFilesOneOrMore": "Fitxers PDF (un o més)", + "oneOrMoreRtf": "Un o més fitxers RTF", + "svgGraphics": "Gràfics SVG", + "tiffImages": "Imatges TIFF", + "webpImages": "Imatges WebP" + } + }, + "howItWorks": { + "title": "Com funciona", + "step1Title": "Puja el fitxer", + "step1": "Fes clic o arrossega i deixa anar el fitxer per començar", + "step2Title": "Processa", + "step2": "Fes clic al botó de processament per començar", + "step3Title": "Descarrega", + "step3": "Desa el fitxer processat a l'instant" + }, + "relatedTools": { + "title": "Eines PDF relacionades" + }, + "loader": { + "processing": "S'està processant..." + }, + "alert": { + "title": "Avís", + "ok": "D'acord" + }, + "preview": { + "title": "Previsualització del document", + "downloadAsPdf": "Descarrega com a PDF", + "close": "Tanca" + }, + "settings": { + "title": "Configuració", + "shortcuts": "Dreceres de teclat", + "preferences": "Preferències", + "displayPreferences": "Preferències de visualització", + "searchShortcuts": "Cerca dreceres...", + "shortcutsInfo": "Mantén premudes les tecles per definir una drecera. Els canvis es desen automàticament.", + "shortcutsWarning": "⚠️ Evita les dreceres habituals del navegador (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N, etc.), perquè poden no funcionar correctament.", + "import": "Importa", + "export": "Exporta", + "resetToDefaults": "Restableix els valors predeterminats", + "fullWidthMode": "Mode d'amplada completa", + "fullWidthDescription": "Utilitza tota l'amplada de la pantalla per a totes les eines en lloc d'un contenidor centrat", + "compactMode": "Mode compacte", + "compactModeDescription": "Mostra les eines en una llista compacta en lloc de targetes", + "advancedSettings": "Configuració avançada", + "advancedSettingsDescription": "Configura els mòduls de processament externs (PyMuPDF, Ghostscript, CoherentPDF)", + "settingsAutoSaved": "La configuració es desa automàticament", + "clickToSet": "Fes clic per definir-la", + "pressKeys": "Prem les tecles...", + "warnings": { + "alreadyInUse": "La drecera ja s'utilitza", + "assignedTo": "ja està assignada a:", + "chooseDifferent": "Tria una altra drecera.", + "reserved": "Avís de drecera reservada", + "commonlyUsed": "s'utilitza habitualment per a:", + "unreliable": "Aquesta drecera pot no funcionar correctament o pot entrar en conflicte amb el navegador o el sistema.", + "useAnyway": "La vols utilitzar igualment?", + "resetTitle": "Restableix les dreceres", + "resetMessage": "Segur que vols restablir totes les dreceres als valors predeterminats?

Aquesta acció no es pot desfer.", + "importSuccessTitle": "Importació correcta", + "importSuccessMessage": "Les dreceres s'han importat correctament!", + "importFailTitle": "Error d'importació", + "importFailMessage": "No s'han pogut importar les dreceres. El format del fitxer no és vàlid." + } + }, + "warning": { + "title": "Advertiment", + "cancel": "Cancel·la", + "proceed": "Continua" + }, + "compliance": { + "title": "Les dades no surten mai del dispositiu", + "weKeep": "Mantenim", + "yourInfoSafe": "la teva informació segura", + "byFollowingStandards": "seguint les normes internacionals de seguretat.", + "processingLocal": "Tot el processament es fa localment al teu dispositiu.", + "gdpr": { + "title": "Compliment del RGPD", + "description": "Protegeix les dades personals i la privadesa de les persones a la Unió Europea." + }, + "ccpa": { + "title": "Compliment de la CCPA", + "description": "Reconeix drets als residents de Califòrnia sobre la recopilació, l'ús i la compartició de la seva informació personal." + }, + "hipaa": { + "title": "Compliment de la HIPAA", + "description": "Estableix garanties per tractar informació sanitària sensible al sistema sanitari dels Estats Units." + } + }, + "faq": { + "title": "Preguntes", + "questions": "freqüents", + "sectionTitle": "Preguntes freqüents", + "isFree": { + "question": "BentoPDF és realment gratuït?", + "answer": "Sí, del tot. Totes les eines de BentoPDF són 100 % gratuïtes, sense límits de fitxers, registres ni marques d'aigua. Creiem que tothom hauria de tenir accés a eines PDF senzilles i potents sense haver de pagar." + }, + "areFilesSecure": { + "question": "Els meus fitxers són segurs? On es processen?", + "answer": "Els teus fitxers es mantenen tan segurs com és possible perquè no surten mai de l'ordinador. Tot el processament es fa directament al navegador web (al dispositiu). No enviem mai els fitxers a cap servidor, de manera que mantens la privadesa i el control dels documents." + }, + "platforms": { + "question": "Funciona a Mac, Windows i dispositius mòbils?", + "answer": "Sí! Com que BentoPDF funciona íntegrament al navegador, és compatible amb qualsevol sistema operatiu que tingui un navegador modern, com ara Windows, macOS, Linux, iOS i Android." + }, + "gdprCompliant": { + "question": "BentoPDF compleix el RGPD?", + "answer": "Sí. BentoPDF compleix plenament el RGPD. Com que els fitxers es processen localment al navegador i mai no els recopilem ni els transmetem a cap servidor, no tenim accés a les teves dades. Això garanteix que sempre mantens el control dels documents." + }, + "dataStorage": { + "question": "Emmagatzemeu o feu seguiment dels meus fitxers?", + "answer": "No. Mai no desem, seguim ni registrem els teus fitxers. Tot el que fas a BentoPDF té lloc a la memòria del navegador i desapareix quan tanques la pàgina. No hi ha càrregues de fitxers, historials ni servidors implicats." + }, + "different": { + "question": "Què diferencia BentoPDF d'altres eines PDF?", + "answer": "La majoria d'eines PDF envien els fitxers a un servidor per processar-los. BentoPDF no ho fa mai. Utilitzem tecnologia web moderna i segura per processar els fitxers directament al navegador. Això permet més rapidesa, més privadesa i més tranquil·litat." + }, + "browserBased": { + "question": "Com protegeix les meves dades el processament al navegador?", + "answer": "Com que funciona íntegrament al navegador, BentoPDF garanteix que els fitxers no surten mai del dispositiu. Això elimina els riscos de pirateig de servidors, filtracions de dades o accessos no autoritzats. Els teus fitxers sempre són teus." + }, + "analytics": { + "question": "Utilitzeu galetes o eines d'analítica per fer-me seguiment?", + "answer": "No. BentoPDF no et fa cap seguiment. No utilitzem galetes, analítica ni scripts de seguiment, ni tan sols al web públic bentopdf.com. No sabem mai qui ets ni què fas amb les eines." + } + }, + "testimonials": { + "title": "Què diuen els nostres", + "users": "usuaris", + "say": "sobre nosaltres" + }, + "support": { + "title": "T'agrada la meva feina?", + "description": "BentoPDF és un projecte fet amb passió per oferir a tothom un conjunt d'eines PDF gratuït, privat i potent. Si et resulta útil, planteja't donar suport al seu desenvolupament. Cada cafè ajuda!", + "buyMeCoffee": "Convida'm a un cafè" + }, + "footer": { + "copyright": "© 2026 BentoPDF. Tots els drets reservats.", + "version": "Versió", + "company": "Empresa", + "aboutUs": "Sobre nosaltres", + "faqLink": "Preguntes freqüents", + "contactUs": "Contacta amb nosaltres", + "legal": "Informació legal", + "termsAndConditions": "Termes i condicions", + "privacyPolicy": "Política de privadesa", + "followUs": "Segueix-nos", + "privacyShort": "Privadesa", + "termsShort": "Condicions" + }, + "merge": { + "title": "Fusiona PDF", + "description": "Combina fitxers sencers o selecciona pàgines concretes per fusionar-les en un document nou.", + "fileMode": "Mode de fitxers", + "pageMode": "Mode de pàgines", + "howItWorks": "Com funciona:", + "fileModeInstructions": [ + "Fes clic a la icona i arrossega-la per canviar l'ordre dels fitxers.", + "Al camp «Pàgines» de cada fitxer, pots indicar intervals (p. ex., «1-3, 5») per fusionar només aquestes pàgines.", + "Deixa el camp «Pàgines» buit per incloure totes les pàgines del fitxer." + ], + "pageModeInstructions": [ + "A continuació es mostren totes les pàgines dels PDF que has pujat.", + "Arrossega i deixa anar les miniatures de cada pàgina per establir l'ordre exacte del fitxer nou." + ], + "mergePdfs": "Fusiona PDF" + }, + "common": { + "page": "Pàgina", + "pages": "Pàgines", + "of": "de", + "download": "Descarrega", + "cancel": "Cancel·la", + "save": "Desa", + "delete": "Suprimeix", + "edit": "Edita", + "add": "Afegeix", + "remove": "Elimina", + "loading": "S'està carregant...", + "loadingPageCount": "S'estan carregant les pàgines...", + "error": "Error", + "success": "Correcte", + "file": "Fitxer", + "files": "Fitxers", + "close": "Tanca", + "convert": "Converteix", + "unknownError": "Error desconegut" + }, + "about": { + "hero": { + "title": "Les eines PDF han de ser", + "subtitle": "ràpides, privades i gratuïtes.", + "noCompromises": "Sense concessions." + }, + "mission": { + "title": "La nostra", + "titleDim": "missió", + "description": "Oferir el conjunt d'eines PDF més complet, respectuós amb la privadesa i que no exigeixi cap pagament. Creiem que les eines essencials per treballar amb documents han de ser accessibles per a tothom, arreu i sense barreres." + }, + "philosophy": { + "label": "La nostra filosofia fonamental", + "title": "La privadesa, primer. Sempre.", + "description": "Totes les eines de BentoPDF funcionen localment al navegador. Els fitxers no es pugen mai a cap servidor, ningú més no veu els documents i no es fa seguiment de la teva activitat." + }, + "whyBentopdf": { + "title": "Per què", + "speed": { + "title": "Pensat per ser ràpid", + "description": "No cal pujar ni descarregar fitxers al servidor. Els fitxers es processen al navegador mitjançant WebAssembly, sense haver d'esperar cap servidor." + }, + "free": { + "title": "Ús gratuït", + "description": "Totes les eines són gratuïtes, sense proves, subscripcions ni marques d'aigua. El desenvolupament es finança amb llicències comercials." + }, + "noAccount": { + "title": "Sense compte", + "description": "Obre qualsevol eina i comença de seguida. Sense correu electrònic, contrasenya ni dades personals." + }, + "openSource": { + "title": "Codi obert", + "description": "El codi és públic a GitHub amb llicència AGPL-3.0, perquè puguis comprovar com es tracten els fitxers. Es basa en projectes de codi obert com PDF.js, pdf-lib, qpdf i LibreOffice." + } + }, + "cta": { + "title": "Ja ho tens tot a punt per començar?", + "description": "Uneix-te als milers d'usuaris que confien en BentoPDF per treballar amb documents cada dia. Descobreix com la privadesa i el rendiment marquen la diferència.", + "button": "Explora totes les eines" + } + }, + "contact": { + "title": "Contacta amb nosaltres", + "subtitle": "Ens agradarà saber de tu. Si tens una pregunta, un comentari o una proposta de funcionalitat, no dubtis a escriure'ns.", + "email": "Pots contactar directament amb nosaltres per correu electrònic a:" + }, + "licensing": { + "title": "Llicències per a", + "subtitle": "Tria la llicència que s'adapti a les teves necessitats." + }, + "multiTool": { + "uploadPdfs": "Puja fitxers PDF", + "upload": "Puja", + "addBlankPage": "Afegeix una pàgina en blanc", + "edit": "Edita:", + "undo": "Desfés", + "redo": "Refés", + "reset": "Restableix", + "selection": "Selecció:", + "selectAll": "Selecciona-ho tot", + "deselectAll": "Desselecciona-ho tot", + "rotate": "Gira:", + "rotateLeft": "Esquerra", + "rotateRight": "Dreta", + "transform": "Transforma:", + "duplicate": "Duplica", + "split": "Divideix", + "clear": "Neteja:", + "delete": "Suprimeix", + "download": "Descarrega:", + "downloadSelected": "Descarrega la selecció", + "exportPdf": "Exporta el PDF", + "uploadPdfFiles": "Selecciona fitxers PDF o d'imatge", + "dragAndDrop": "Arrossega i deixa anar aquí fitxers PDF o imatges, o fes clic per seleccionar-los", + "selectFiles": "Selecciona fitxers", + "dropToAdd": "Deixa anar fitxers PDF o d'imatge per afegir-los", + "convertingImages": "S'estan convertint les imatges a PDF...", + "failedToConvertImages": "No s'han pogut convertir les imatges a PDF.", + "renderingPages": "S'estan representant les pàgines...", + "actions": { + "duplicatePage": "Duplica aquesta pàgina", + "deletePage": "Suprimeix aquesta pàgina", + "insertPdf": "Insereix un PDF després d'aquesta pàgina", + "toggleSplit": "Activa o desactiva la divisió després d'aquesta pàgina" + }, + "pleaseWait": "Espera, si us plau", + "pagesRendering": "Encara s'estan representant les pàgines. Espera, si us plau...", + "noPagesSelected": "No hi ha pàgines seleccionades", + "selectOnePage": "Selecciona com a mínim una pàgina per descarregar-la.", + "noPages": "No hi ha pàgines", + "noPagesToExport": "No hi ha pàgines per exportar.", + "renderingTitle": "S'estan generant les previsualitzacions de les pàgines", + "errorRendering": "No s'han pogut generar les miniatures de les pàgines", + "error": "Error", + "failedToLoad": "No s'ha pogut carregar" + }, + "simpleMode": { + "title": "Eines PDF", + "subtitle": "Selecciona una eina per començar" + }, + "disabledTool": { + "title": "Eina no disponible", + "heading": "Aquesta eina s'ha desactivat", + "message": "Aquesta eina no està disponible en aquesta instal·lació. Contacta amb l'administrador per obtenir més informació.", + "backHome": "Torna a l'inici" + } +} diff --git a/public/locales/ca/tools.json b/public/locales/ca/tools.json new file mode 100644 index 00000000..6cf611bf --- /dev/null +++ b/public/locales/ca/tools.json @@ -0,0 +1,1327 @@ +{ + "categories": { + "popularTools": "Eines populars", + "editAnnotate": "Edita i anota", + "convertToPdf": "Converteix a PDF", + "convertFromPdf": "Converteix des de PDF", + "organizeManage": "Organitza i gestiona", + "optimizeRepair": "Optimitza i repara", + "securePdf": "Protegeix els PDF", + "input": "Entrada", + "output": "Sortida" + }, + "convertToPdf": "Converteix a PDF", + "pdfMultiTool": { + "name": "Eina PDF multifunció", + "subtitle": "Fusiona, divideix, organitza, suprimeix, gira, afegeix pàgines en blanc, extreu i duplica en una sola interfície." + }, + "mergePdf": { + "name": "Fusiona PDF", + "subtitle": "Combina diversos PDF en un sol fitxer. Conserva els marcadors.", + "retainPageLabels": "Conserva les etiquetes de pàgina originals de cada fitxer", + "retainPageLabelsHint": "Desactivat (predeterminat): el PDF fusionat utilitza la numeració natural de pàgines, d'1 a N. Activat: es conserven les etiquetes originals de cada fitxer (pot generar etiquetes duplicades).", + "removeDuplicateFonts": "Elimina els tipus de lletra duplicats" + }, + "splitPdf": { + "name": "Divideix PDF", + "subtitle": "Extreu un interval de pàgines en un PDF nou." + }, + "compressPdf": { + "name": "Comprimeix PDF", + "subtitle": "Redueix la mida del fitxer PDF.", + "algorithmLabel": "Algorisme de compressió", + "condense": "Condense (recomanat)", + "photon": "Photon (per a PDF amb moltes fotografies)", + "condenseInfo": "Condense utilitza compressió avançada: elimina dades innecessàries, optimitza les imatges i redueix els tipus de lletra als caràcters utilitzats. És l'opció ideal per a la majoria de PDF.", + "photonInfo": "Photon converteix les pàgines en imatges. Utilitza'l per a PDF amb moltes fotografies o documents escanejats.", + "photonWarning": "Advertiment: el text deixarà de ser seleccionable i els enllaços no funcionaran.", + "levelLabel": "Nivell de compressió", + "light": "Lleugera (conserva la qualitat)", + "balanced": "Equilibrada (recomanada)", + "aggressive": "Agressiva (fitxers més petits)", + "extreme": "Extrema (compressió màxima)", + "grayscale": "Converteix a escala de grisos", + "grayscaleHint": "Redueix la mida del fitxer eliminant la informació de color", + "customSettings": "Configuració personalitzada", + "customSettingsHint": "Ajusta els paràmetres de compressió:", + "outputQuality": "Qualitat de sortida", + "resizeImagesTo": "Redimensiona les imatges a", + "onlyProcessAbove": "Processa només les que superin", + "removeMetadata": "Elimina les metadades", + "subsetFonts": "Redueix els tipus de lletra (elimina els glifs no utilitzats)", + "removeThumbnails": "Elimina les miniatures incrustades", + "compressButton": "Comprimeix PDF" + }, + "pdfEditor": { + "name": "Editor de PDF", + "subtitle": "Anota, ressalta, oculta informació, comenta, afegeix formes o imatges, cerca i visualitza PDF." + }, + "editPdfText": { + "name": "Edita el text d'un PDF", + "subtitle": "Fes clic a qualsevol paràgraf i edita'l directament amb reajustament automàtic del text, tipus de lletra i estils.", + "saved": "El PDF s'ha desat correctament! ({{size}} KB)", + "failedToLoad": "No s'ha pogut carregar l'editor de text PDF.", + "loadingFonts": "S'estan carregant els tipus de lletra...", + "fontsUnavailable": "No s'ha pogut descarregar un tipus de lletra necessari per a alguns caràcters. Comprova la connexió i torna-ho a provar; en cas contrari, aquests caràcters podrien no aparèixer correctament al PDF desat.", + "howToUpload": "Puja un PDF", + "howToClick": "Fes clic en un paràgraf", + "howToRestyle": "Canvia l'estil del contingut", + "relatedPdfEditor": "Anota i marca els PDF", + "relatedSignPdf": "Dibuixa, escriu o puja una signatura", + "relatedWatermark": "Estampa text o imatges", + "relatedStamps": "Col·loca segells a les pàgines", + "relatedCompress": "Redueix la mida del fitxer", + "relatedOrganizeName": "Organitza PDF", + "relatedOrganize": "Reordena i gestiona les pàgines", + "open": "Obre", + "export": "Exporta", + "filterAll": "Tot", + "filterText": "Text", + "filterImages": "Imatges", + "filterShapes": "Formes", + "addText": "Afegeix text", + "langAuto": "Automàtic", + "change": "Canvia", + "ignore": "Ignora", + "next": "Següent", + "replace": "Substitueix", + "replaceAll": "Substitueix-ho tot", + "emptyState": "Obre un PDF per editar-ne el text, les imatges i les formes.", + "choosePdf": "Tria un PDF…", + "font": "Tipus de lletra", + "systemFonts": "Tipus de lletra del sistema", + "color": "Color", + "alignment": "Alineació", + "style": "Estil", + "lists": "Llistes", + "outline": "Contorn", + "spacing": "Espaiat", + "direction": "Direcció", + "dirAuto": "Automàtic", + "pathStroke": "Traç del camí", + "tipBackToUpload": "Torna a la càrrega", + "tipOpenAnotherPdf": "Obre un altre PDF", + "tipExportS": "Exporta (⌘S)", + "tipUndoZ": "Desfés (⌘Z)", + "tipRedoZ": "Refés (⇧⌘Z)", + "tipFileDetails": "Detalls del fitxer", + "tipEditExistingContent": "Edita el contingut existent", + "tipWhatTheEditToolTouchesNarrowTheScopeOnBus": "Què modifica l'eina d'edició? Limita l'abast a les pàgines complexes", + "tipClickThePageToAddATextBox": "Fes clic a la pàgina per afegir un quadre de text", + "tipSpellCheckLanguage": "Idioma del corrector ortogràfic", + "tipFindAndReplace": "Cerca i substitueix", + "tipAddAnImage": "Afegeix una imatge", + "tipCopyThisPageSTextInReadingOrder": "Copia el text d'aquesta pàgina en ordre de lectura", + "tipCheckTheSpellingOfThisPage": "Comprova l'ortografia d'aquesta pàgina", + "tipShowOrHideTheFormatPanel": "Mostra o amaga el tauler de format", + "phReplaceWith": "Substitueix per", + "tipAddToYourDictionary": "Afegeix al teu diccionari", + "tipClose": "Tanca", + "phFind": "Cerca", + "tipPreviousMatch": "Coincidència anterior", + "tipNextMatch": "Coincidència següent", + "tipRotateLeft90": "Gira 90° a l'esquerra", + "tipRotateRight90": "Gira 90° a la dreta", + "tipFlipHorizontal": "Inverteix horitzontalment", + "tipFlipVertical": "Inverteix verticalment", + "tipReplaceThisImageKeepingItsPlacement": "Substitueix aquesta imatge mantenint-ne la posició", + "tipAlternateTextForAccessibilityTaggedPdfs": "Text alternatiu per a l'accessibilitat (PDF etiquetats)", + "tipBringToFront": "Porta al davant", + "tipSendToBack": "Envia al darrere", + "tipDuplicate": "Duplica", + "tipDelete": "Suprimeix", + "tipAlignLeftEdges": "Alinea les vores esquerres", + "tipAlignHorizontalCenters": "Alinea els centres horitzontals", + "tipAlignRightEdges": "Alinea les vores dretes", + "tipAlignTops": "Alinea les vores superiors", + "tipAlignVerticalCenters": "Alinea els centres verticals", + "tipAlignBottoms": "Alinea les vores inferiors", + "tipDistributeHorizontally": "Distribueix horitzontalment", + "tipDistributeVertically": "Distribueix verticalment", + "tipFontSize": "Mida del tipus de lletra", + "tipListFontsInstalledOnThisDeviceChromium": "Mostra els tipus de lletra instal·lats al dispositiu (Chromium)", + "tipCustomTextColor": "Color de text personalitzat", + "tipAlignLeft": "Alinea a l'esquerra", + "tipAlignCenter": "Centra", + "tipAlignRight": "Alinea a la dreta", + "tipJustify": "Justifica", + "tipBold": "Negreta", + "tipItalic": "Cursiva", + "tipUnderline": "Subratllat", + "tipStrikethrough": "Ratllat", + "tipSuperscript": "Superíndex", + "tipSubscript": "Subíndex", + "tipToggleBulletedItem": "Activa o desactiva les vinyetes", + "tipToggleNumberedItem": "Activa o desactiva la numeració", + "tipNumberedListMarkerStyle": "Estil del marcador de la llista numerada", + "tipDecreaseListIndentTab": "Redueix el sagnat de la llista (⇧Tab)", + "tipIncreaseListIndentTab": "Augmenta el sagnat de la llista (Tab)", + "tipTextOutlineColor": "Color del contorn del text", + "tipOutlineWidth0None": "Amplada del contorn (0 = cap)", + "tipFromContent": "Segons el contingut", + "tipLeftToRight": "D'esquerra a dreta", + "tipRightToLeft": "De dreta a esquerra", + "tipStrokeColor": "Color del traç", + "tipStrokeWidth": "Amplada del traç", + "tipZoomPresets": "Valors de zoom predefinits", + "tipZoomOut": "Allunya", + "tipZoomIn": "Apropa", + "tipFitPage": "Ajusta la pàgina", + "tipPreviousPage": "Pàgina anterior", + "tipTypeAPageNumberAndPressEnter": "Escriu un número de pàgina i prem Retorn", + "tipNextPage": "Pàgina següent", + "tipDocumentProperties": "Propietats del document", + "tipHideBar": "Amaga la barra", + "tipShowBar": "Mostra la barra" + }, + "jpgToPdf": { + "name": "JPG a PDF", + "subtitle": "Crea un PDF a partir d'imatges JPG, JPEG i JPEG2000 (JP2/JPX)." + }, + "signPdf": { + "name": "Signa PDF", + "subtitle": "Dibuixa, escriu o puja la teva signatura." + }, + "cropPdf": { + "name": "Retalla PDF", + "subtitle": "Retalla els marges de cada pàgina del PDF." + }, + "extractPages": { + "name": "Extreu pàgines", + "subtitle": "Desa una selecció de pàgines com a fitxers nous." + }, + "duplicateOrganize": { + "name": "Duplica i organitza", + "subtitle": "Duplica, reordena i suprimeix pàgines.", + "saveChanges": "Desa els canvis" + }, + "deletePages": { + "name": "Suprimeix pàgines", + "subtitle": "Elimina pàgines concretes del document." + }, + "editBookmarks": { + "name": "Edita els marcadors", + "subtitle": "Afegeix, edita, importa, suprimeix i extreu els marcadors d'un PDF.", + "autoExtractExisting": "Extreu automàticament els marcadors existents", + "importSectionTitle": "O importa marcadors", + "csvFormat": "Format CSV: títol,pàgina,nivell", + "jsonFormat": "Format JSON", + "uploadCsvAction": "Fes clic per pujar un CSV", + "uploadJsonAction": "Fes clic per pujar un JSON", + "orDragHere": "o arrossega'l aquí" + }, + "tableOfContents": { + "name": "Taula de continguts", + "subtitle": "Genera una pàgina de taula de continguts a partir dels marcadors del PDF.", + "titleLabel": "Títol de la taula de continguts", + "fontSize": "Mida del tipus de lletra", + "fontFamily": "Família tipogràfica", + "addBookmark": "Afegeix un marcador per a la pàgina de la taula de continguts", + "generateButton": "Genera la taula de continguts", + "selectPdfFirst": "Selecciona primer un fitxer PDF.", + "statusReadingFileMainThread": "S'està llegint el fitxer (fil principal)...", + "statusGenerating": "S'està generant la taula de continguts...", + "errorReadingFileWithMessage": "Error en llegir el fitxer: {{message}}", + "successGeneratedDownloadStarted": "La taula de continguts s'ha generat correctament! S'ha iniciat la descàrrega.", + "workerErrorWithMessage": "Error: {{message}}", + "workerErrorOccurred": "S'ha produït un error en el procés de treball. Consulta la consola per obtenir-ne detalls." + }, + "pageNumbers": { + "name": "Números de pàgina", + "subtitle": "Insereix números de pàgina al document." + }, + "addPageLabels": { + "name": "Afegeix etiquetes de pàgina", + "subtitle": "Aplica etiquetes de pàgina amb números romans, prefixos i valors inicials personalitzats.", + "uploadHint": "Fitxer PDF", + "howLabelsWorkTitle": "Com funcionen les etiquetes", + "howLabelsWorkStep1": "Deixa buit l'interval de pàgines per etiquetar-les totes.", + "howLabelsWorkStep2": "Utilitza estils com els números romans per a les pàgines preliminars i la numeració decimal per al cos principal.", + "howLabelsWorkStep3": "Utilitza un prefix i el valor inicial 0 per crear etiquetes com A-0, A-1 i A-2.", + "howLabelsWorkStep4": "Activa la numeració contínua quan una regla inclogui intervals discontinus com 1-9,30-40 o senars.", + "labelRulesTitle": "Regles d'etiquetatge", + "addRule": "Afegeix una regla", + "removeExistingLabels": "Elimina les etiquetes existents abans d'aplicar aquestes regles", + "cpdfNote": "Aquesta eina utilitza CoherentPDF al navegador. Si CoherentPDF no està configurat, se't demanarà que el configuris als paràmetres WASM.", + "processButton": "Afegeix etiquetes de pàgina", + "howItWorksUploadTitle": "Puja el fitxer", + "howItWorksUploadDescription": "Tria el PDF del qual vols canviar les etiquetes.", + "howItWorksDefineTitle": "Defineix les regles", + "howItWorksDefineDescription": "Estableix una o més regles amb intervals de pàgines, estils, prefixos i valors inicials.", + "howItWorksDownloadTitle": "Descarrega", + "howItWorksDownloadDescription": "Desa el PDF actualitzat amb les noves etiquetes de pàgina.", + "relatedToolsTitle": "Eines PDF relacionades", + "faqWhatIsQuestion": "Què és una etiqueta de pàgina?", + "faqWhatIsAnswer": "Una etiqueta de pàgina és la numeració que mostren els lectors de PDF a les miniatures, la navegació i els quadres d'impressió. No afegeix text imprès a la pàgina.", + "faqMixQuestion": "Puc barrejar números romans i números habituals?", + "faqMixAnswer": "Sí. Afegeix diverses regles, per exemple, 1-4 amb LowercaseRoman i 5-20 amb DecimalArabic.", + "faqPrivacyQuestion": "Es manté la privadesa dels meus fitxers?", + "faqPrivacyAnswer": "Sí. El processament es fa al navegador mitjançant CoherentPDF WASM. El PDF no surt del dispositiu.", + "invalidFileTitle": "Fitxer no vàlid", + "invalidFileMessage": "Puja un fitxer PDF vàlid.", + "loadingPdf": "S'està carregant el PDF...", + "protectedPdfTitle": "PDF protegit", + "protectedPdfMessage": "Aquest PDF està protegit amb contrasenya. Utilitza primer l'eina Desxifra o Canvia els permisos.", + "loadErrorMessage": "No s'ha pogut carregar el fitxer PDF. Pot ser que no sigui vàlid, que estigui malmès o que estigui protegit amb contrasenya.", + "fileMeta": "{{size}} • {{count}} pàgines", + "ruleTitle": "Regla d'etiquetatge {{number}}", + "pageRangeLabel": "Interval de pàgines", + "pageRangePlaceholder": "Totes les pàgines o, p. ex., 1-4, 7, senars", + "labelStyleLabel": "Estil d'etiqueta", + "labelPrefixLabel": "Prefix de l'etiqueta", + "labelPrefixPlaceholder": "Prefix opcional, p. ex., A-", + "startValueLabel": "Valor inicial", + "continueNumbering": "Continua la numeració entre intervals discontinus", + "examplesNote": "Exemples: 1-4 per a les pàgines preliminars en romans, 15-20 amb el prefix A- i valor inicial 0, o senars amb numeració contínua.", + "uploadFirstMessage": "Primer has de pujar un fitxer PDF.", + "applyingLabels": "S'estan aplicant les etiquetes de pàgina...", + "invalidRangeMessage": "La regla {{number}} té un interval de pàgines no vàlid: {{range}}", + "allPages": "totes les pàgines", + "emptyOutputMessage": "CoherentPDF ha generat un fitxer buit.", + "successMessage": "Les etiquetes de pàgina s'han afegit correctament!", + "processErrorMessage": "No s'han pogut afegir les etiquetes de pàgina.", + "styleOptions": { + "DecimalArabic": "Numeració decimal aràbiga", + "LowercaseRoman": "Números romans en minúscula", + "UppercaseRoman": "Números romans en majúscula", + "LowercaseLetters": "Lletres en minúscula", + "UppercaseLetters": "Lletres en majúscula", + "NoLabelPrefixOnly": "Sense número, només prefix" + } + }, + "batesNumbering": { + "name": "Numeració Bates", + "subtitle": "Afegeix números Bates seqüencials a un o més fitxers PDF." + }, + "addWatermark": { + "name": "Afegeix una marca d'aigua", + "subtitle": "Estampa un text o una imatge a les pàgines del PDF.", + "applyToAllPages": "Aplica a totes les pàgines" + }, + "headerFooter": { + "name": "Capçalera i peu de pàgina", + "subtitle": "Afegeix text a la part superior i inferior de les pàgines." + }, + "invertColors": { + "name": "Inverteix els colors", + "subtitle": "Crea una versió del PDF amb «mode fosc»." + }, + "scannerEffect": { + "name": "Efecte d'escàner", + "subtitle": "Fes que el PDF sembli un document escanejat.", + "scanSettings": "Configuració de l'escaneig", + "colorspace": "Espai de color", + "gray": "Gris", + "border": "Vora", + "rotate": "Gir", + "rotateVariance": "Variació del gir", + "brightness": "Brillantor", + "contrast": "Contrast", + "blur": "Desenfocament", + "noise": "Soroll", + "yellowish": "To groguenc", + "resolution": "Resolució", + "processButton": "Aplica l'efecte d'escàner" + }, + "adjustColors": { + "name": "Ajusta els colors", + "subtitle": "Ajusta amb precisió la brillantor, el contrast, la saturació i altres paràmetres del PDF.", + "colorSettings": "Configuració del color", + "brightness": "Brillantor", + "contrast": "Contrast", + "saturation": "Saturació", + "hueShift": "Desplaçament del to", + "temperature": "Temperatura", + "tint": "Matís", + "gamma": "Gamma", + "sepia": "Sèpia", + "processButton": "Aplica els ajustos de color" + }, + "backgroundColor": { + "name": "Color de fons", + "subtitle": "Canvia el color de fons del PDF." + }, + "changeTextColor": { + "name": "Canvia el color del text", + "subtitle": "Canvia el color del text del PDF." + }, + "addStamps": { + "name": "Afegeix segells", + "subtitle": "Afegeix segells d'imatge al PDF mitjançant la barra d'anotacions.", + "usernameLabel": "Nom d'usuari del segell", + "usernamePlaceholder": "Escriu el teu nom (per als segells)", + "usernameHint": "Aquest nom apareixerà als segells que creïs.", + "saveButton": "Desa el PDF amb segells" + }, + "removeAnnotations": { + "name": "Elimina les anotacions", + "subtitle": "Elimina els comentaris, els ressaltats i els enllaços." + }, + "pdfFormFiller": { + "name": "Emplena formularis PDF", + "subtitle": "Emplena formularis directament al navegador. També admet formularis XFA.", + "uploadHint": "Puja un PDF amb camps de formulari. Emplena'ls directament al visor següent i fes clic al botó per desar i descarregar el formulari emplenat. També s'admeten formularis XFA.", + "xfaNoteTitle": "Nota sobre els formularis XFA:", + "xfaNoteDescription": "XFA (XML Forms Architecture) és un format antic que només admeten alguns visors PDF, com BentoPDF i Firefox. Si obres un PDF XFA en un altre programa i veus pàgines en blanc o no hi apareixen camps, vol dir que aquest visor no admet XFA. Per visualitzar i emplenar correctament els formularis XFA, utilitza Firefox o l'eina d'emplenament de formularis de BentoPDF." + }, + "createPdfForm": { + "name": "Crea un formulari PDF", + "subtitle": "Crea formularis PDF emplenables arrossegant i deixant anar camps de text.", + "orSeparator": "O BÉ", + "createBlankPdf": "Crea un PDF en blanc" + }, + "removeBlankPages": { + "name": "Elimina les pàgines en blanc", + "subtitle": "Detecta i suprimeix automàticament les pàgines en blanc.", + "sensitivityHint": "Més alt = més estricte, només pàgines completament buides. Més baix = permet pàgines amb una mica de contingut." + }, + "imageToPdf": { + "name": "Imatges a PDF", + "subtitle": "Converteix JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC i WebP a PDF." + }, + "pngToPdf": { + "name": "PNG a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges PNG." + }, + "webpToPdf": { + "name": "WebP a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges WebP." + }, + "svgToPdf": { + "name": "SVG a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges SVG." + }, + "bmpToPdf": { + "name": "BMP a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges BMP." + }, + "heicToPdf": { + "name": "HEIC a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges HEIC." + }, + "tiffToPdf": { + "name": "TIFF a PDF", + "subtitle": "Crea un PDF a partir d'una o més imatges TIFF." + }, + "textToPdf": { + "name": "Text a PDF", + "subtitle": "Converteix un fitxer de text pla a PDF.", + "uploadFiles": "Puja fitxers", + "typeText": "Escriu text", + "acceptedFormats": "Fitxers de text (.txt)", + "fontFamily": "Família tipogràfica", + "fontSize": "Mida del tipus de lletra", + "textColor": "Color del text", + "pageSize": "Mida de pàgina", + "rtlAutoDetectNote": "Els idiomes que s'escriuen de dreta a esquerra (àrab, urdú, persa, etc.) es detecten automàticament i s'ajusta la direcció del text.", + "createButton": "Crea un PDF" + }, + "jsonToPdf": { + "name": "JSON a PDF", + "subtitle": "Converteix fitxers JSON a PDF.", + "noteLabel": "Nota:", + "noteText": "Només s'admeten els fitxers JSON creats per l'eina de conversió de PDF a JSON. Els fitxers JSON estàndard d'altres eines no funcionaran.", + "acceptedFormats": "Diversos fitxers JSON", + "status": { + "selectAtLeastOne": "Selecciona com a mínim un fitxer JSON", + "selectedReady": "{{count}} fitxer(s) seleccionat(s). Tot a punt per convertir!", + "readingFiles": "S'estan llegint els fitxers (fil principal)...", + "converting": "S'estan convertint els fitxers JSON a PDF...", + "readError": "Error en llegir els fitxers: {{message}}", + "creatingZip": "S'està creant el fitxer ZIP...", + "success": "Els fitxers JSON s'han convertit correctament a PDF! S'ha iniciat la descàrrega del ZIP.", + "zipError": "Error en crear el ZIP: {{message}}", + "workerError": "Error del procés de treball: {{message}}", + "getStarted": "Selecciona fitxers JSON per començar" + } + }, + "pdfToJpg": { + "name": "PDF a JPG", + "subtitle": "Converteix cada pàgina del PDF en una imatge JPG.", + "imageQuality": "Qualitat de la imatge", + "imageQualityExplanation": "Com més qualitat, més mida del fitxer", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a JPG!", + "conversionError": "No s'ha pogut convertir el PDF a JPG. Pot ser que el fitxer estigui malmès." + }, + "loader": { + "converting": "S'està convertint a JPG..." + } + }, + "pdfToPng": { + "name": "PDF a PNG", + "subtitle": "Converteix cada pàgina del PDF en una imatge PNG.", + "imageScale": "Escala de la imatge", + "imageScaleExplanation": "Com més escala, més qualitat, però també més mida del fitxer", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a PNG!", + "conversionError": "No s'ha pogut convertir el PDF a PNG. Pot ser que el fitxer estigui malmès." + }, + "loader": { + "converting": "S'està convertint a PNG..." + } + }, + "pdfToWebp": { + "name": "PDF a WebP", + "subtitle": "Converteix cada pàgina del PDF en una imatge WebP.", + "imageQuality": "Qualitat de la imatge", + "imageQualityExplanation": "Com més qualitat, més mida del fitxer", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a WebP!", + "conversionError": "No s'ha pogut convertir el PDF a WebP. Pot ser que el fitxer estigui malmès." + }, + "loader": { + "converting": "S'està convertint a WebP..." + } + }, + "pdfToBmp": { + "name": "PDF a BMP", + "subtitle": "Converteix cada pàgina del PDF en una imatge BMP.", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a BMP!", + "conversionError": "No s'ha pogut convertir el PDF a BMP. Pot ser que el fitxer estigui malmès." + }, + "loader": { + "converting": "S'està convertint a BMP..." + } + }, + "pdfToTiff": { + "name": "PDF a TIFF", + "subtitle": "Converteix cada pàgina del PDF en una imatge TIFF.", + "dpi": "PPP (resolució)", + "dpiExplanation": "Com més PPP, més qualitat d'impressió, però també més mida del fitxer", + "compression": "Compressió", + "compressionExplanation": "LZW i Deflate no comporten pèrdua de qualitat. CCITT Grup 4 és ideal per a documents escanejats en blanc i negre.", + "colorMode": "Mode de color", + "multiPage": "Desa com a TIFF de diverses pàgines (fitxer únic)", + "loadingVips": "S'està carregant el processador d'imatges...", + "converting": "S'està convertint a TIFF...", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a TIFF!", + "conversionError": "No s'ha pogut convertir el PDF a TIFF. Pot ser que el fitxer estigui malmès.", + "conversionOutOfMemory": "El navegador s'ha quedat sense memòria durant la conversió. Prova una resolució inferior o desmarca l'opció TIFF de diverses pàgines per convertir-les d'una en una." + } + }, + "pdfToCbz": { + "name": "PDF a CBZ", + "subtitle": "Converteix un PDF en un fitxer CBZ (arxiu de còmics) per a lectors de còmics.", + "imageFormat": "Format d'imatge", + "quality": "Qualitat de la imatge", + "qualityExplanation": "Com més qualitat, més mida del fitxer", + "scale": "Escala", + "scaleExplanation": "Com més escala, més qualitat en pantalles d'alta resolució", + "grayscale": "Converteix a escala de grisos", + "manga": "Mode manga (de dreta a esquerra)", + "includeMetadata": "Inclou les metadades de ComicInfo.xml", + "titleLabel": "Títol", + "seriesLabel": "Sèrie", + "authorLabel": "Autor(s)", + "numberLabel": "Núm.", + "volumeLabel": "Vol.", + "publisherLabel": "Editorial", + "tagsLabel": "Etiquetes / Gènere", + "yearLabel": "Any de publicació", + "ratingLabel": "Valoració (0-5)", + "converting": "S'està convertint a CBZ...", + "alert": { + "invalidFile": "Fitxer no vàlid", + "invalidFileExplanation": "Puja un fitxer PDF.", + "noFile": "No hi ha cap fitxer", + "noFileExplanation": "Primer has de pujar un fitxer PDF.", + "conversionSuccess": "El PDF s'ha convertit correctament a CBZ!", + "conversionError": "No s'ha pogut convertir el PDF a CBZ. Pot ser que el fitxer estigui malmès." + } + }, + "pdfToGreyscale": { + "name": "PDF a escala de grisos", + "subtitle": "Converteix tots els colors a blanc i negre." + }, + "pdfToJson": { + "name": "PDF a JSON", + "subtitle": "Converteix fitxers PDF al format JSON.", + "convertButton": "Converteix a JSON", + "status": { + "selectAtLeastOne": "Selecciona com a mínim un fitxer PDF", + "selectedReady": "{{count}} fitxer(s) seleccionat(s). Tot a punt per convertir!", + "checkingEncrypted": "S'està comprovant si hi ha PDF xifrats...", + "readingFiles": "S'estan llegint els fitxers (fil principal)...", + "converting": "S'estan convertint els PDF a JSON...", + "readError": "Error en llegir els fitxers: {{message}}", + "creatingZip": "S'està creant el fitxer ZIP...", + "success": "Els PDF s'han convertit correctament a JSON! S'ha iniciat la descàrrega del ZIP.", + "zipError": "Error en crear el ZIP: {{message}}", + "workerError": "Error del procés de treball: {{message}}", + "getStarted": "Selecciona fitxers PDF per començar" + } + }, + "ocrPdf": { + "name": "Reconeixement OCR de PDF", + "subtitle": "Fes que el text d'un PDF es pugui cercar i copiar.", + "languagesInDocument": "Idiomes del document", + "searchLanguagesPlaceholder": "Cerca idiomes...", + "selectedLabel": "Seleccionats:", + "none": "Cap", + "advancedSettingsSummary": "Configuració avançada (recomanada per millorar la precisió)", + "resolutionLabel": "Resolució", + "resolutionStandard": "Estàndard (192 PPP)", + "resolutionHigh": "Alta (288 PPP)", + "resolutionUltra": "Ultra (384 PPP)", + "binarizeLabel": "Binaritza la imatge (millora el contrast dels escanejos nítids)", + "embedFullFontsLabel": "Incrusta els tipus de lletra complets (fitxer més gran, més compatibilitat)", + "whitelistPresetLabel": "Llista de caràcters permesos predefinida", + "whitelistNone": "Cap (tots els caràcters)", + "whitelistAlphanumeric": "Alfanumèrics + puntuació bàsica", + "whitelistNumbersCurrency": "Números + símbols de moneda", + "whitelistLettersOnly": "Només lletres (A-Z, a-z)", + "whitelistNumbersOnly": "Només números (0-9)", + "whitelistInvoice": "Factura / Rebut (números, $, ., -, /)", + "whitelistForms": "Formularis (alfanumèrics + símbols habituals)", + "whitelistCustom": "Personalitzada...", + "whitelistHelp": "Només es reconeixeran aquests caràcters. Deixa el camp buit per admetre'ls tots.", + "whitelistInputLabel": "Caràcters permesos (opcional)", + "whitelistInputPlaceholder": "p. ex., abcdefghijklmnopqrstuvwxyz0123456789$.,", + "startButton": "Inicia l'OCR", + "progressInitializing": "S'està inicialitzant...", + "resultsTitle": "OCR completat", + "resultsDescription": "El PDF amb text cercable està a punt. També pots copiar o descarregar el text extret que apareix a continuació.", + "copyToClipboard": "Copia al porta-retalls", + "downloadTxt": "Descarrega com a .txt", + "downloadSearchablePdf": "Descarrega el PDF amb text cercable", + "statusPrefix": "Estat: {{status}}", + "availabilityInvalidConfig": "Aquesta instal·lació no ofereix cap idioma OCR vàlid. Torna-la a compilar establint VITE_TESSERACT_AVAILABLE_LANGUAGES amb codis Tesseract vàlids.", + "availabilityBundles": "Aquesta instal·lació inclou OCR per a: {{languages}}.", + "noLanguagesAvailable": "No hi ha cap idioma OCR disponible en aquesta instal·lació.", + "noLanguagesTitle": "No s'ha seleccionat cap idioma", + "noLanguagesMessage": "Selecciona com a mínim un idioma per a l'OCR.", + "noFileTitle": "No hi ha cap fitxer", + "noFileMessage": "Primer has de pujar un fitxer PDF.", + "languageUnavailableTitle": "Idioma OCR no disponible", + "ocrErrorTitle": "Error d'OCR", + "ocrErrorMessage": "S'ha produït un error durant el procés d'OCR. És possible que no s'hagi pogut carregar el procés de treball. Torna-ho a provar.", + "howItWorksTitle": "Com funciona:", + "extractTextLabel": "Extreu text:", + "extractTextDescription": "Utilitza Tesseract OCR per reconèixer text d'imatges escanejades o PDF.", + "searchableOutputLabel": "Sortida amb text cercable:", + "searchableOutputDescription": "Crea un PDF nou amb una capa de text invisible que permet cercar-hi text sense alterar-ne l'aparença original.", + "characterFilteringLabel": "Filtratge de caràcters:", + "characterFilteringDescription": "Utilitza llistes de caràcters permesos per excloure els no desitjats i millorar la precisió en documents concrets (factures, formularis, etc.).", + "multiLanguageSupportLabel": "Compatibilitat amb diversos idiomes:", + "multiLanguageSupportDescription": "Selecciona diversos idiomes per als documents amb contingut multilingüe." + }, + "alternateMerge": { + "name": "Alterna i barreja pàgines", + "subtitle": "Fusiona PDF alternant les pàgines de cada fitxer. Conserva els marcadors.", + "retainPageLabels": "Conserva les etiquetes de pàgina originals de cada fitxer", + "retainPageLabelsHint": "Desactivat (predeterminat): el PDF fusionat utilitza la numeració natural de pàgines, d'1 a N. Activat: es conserven les etiquetes originals de cada fitxer (pot generar etiquetes duplicades)." + }, + "duplexCollate": { + "name": "Ordena les cares d'un escaneig a doble cara", + "subtitle": "Reordena un PDF escanejat en dos blocs (tots els anversos i després tots els reversos) perquè quedin aparellats correctament.", + "totalPagesLabel": "Total de pàgines:", + "splitAfterPageLabel": "Divideix després de la pàgina", + "splitAfterPagePlaceholder": "p. ex., 50", + "backBlockOrderLabel": "Ordre del bloc de reversos", + "backBlockReverseOption": "Inverteix el bloc de reversos (habitual en piles capgirades)", + "backBlockKeepOption": "Mantén el bloc de reversos tal com està", + "autoSplitButton": "Detecta automàticament el punt de divisió (meitat)", + "exportGroupedLabel": "Exporta fitxers agrupats segons la llargada del document original", + "pagesPerDocumentLabel": "Pàgines per document original", + "groupedHelp": "Si s'activa, es generarà un ZIP amb un PDF per cada document agrupat.", + "collateButton": "Ordena les cares del PDF", + "invalidFileTitle": "Fitxer no vàlid", + "invalidFileMessage": "Selecciona un fitxer PDF.", + "loadingPdf": "S'està carregant el PDF...", + "loadErrorMessage": "No s'ha pogut carregar el fitxer PDF.", + "fileMeta": "{{size}} - {{count}} pàgines", + "oddPageWarning": "Nombre senar de pàgines ({{count}}). Un escaneig a doble cara hauria de tenir un nombre parell de pàgines; pot faltar o sobrar alguna pàgina.", + "previewSummary": "Bloc d'anversos: {{frontCount}} pàgina(es), bloc de reversos: {{backCount}} pàgina(es). Sortida: {{outputCount}} pàgina(es), en ordre d'anvers i revers.", + "previewMismatchWarning": "Els blocs d'anversos i reversos tenen mides diferents ({{frontCount}} anversos i {{backCount}} reversos). Les {{extraCount}} pàgines sobrants del bloc més llarg s'afegiran al final sense parella.", + "missingFileTitle": "Falta el fitxer", + "missingFileMessage": "Puja un PDF d'almenys 2 pàgines abans de processar-lo.", + "invalidSplitTitle": "Punt de divisió no vàlid", + "invalidSplitMessage": "El punt de divisió ha d'estar entre la pàgina 1 i la {{maxSplit}}.", + "unevenConfirmTitle": "Blocs desiguals", + "unevenConfirmMessage": "El bloc d'anversos té {{frontCount}} pàgina(es) i el de reversos, {{backCount}} pàgina(es).\nNormalment això vol dir que s'ha escanejat una pàgina dues vegades o que en falta una.\n\nLes {{extraCount}} pàgines sense parella s'afegiran al final.\n\nVols continuar igualment?", + "invalidGroupTitle": "Mida del grup no vàlida", + "invalidGroupMessage": "Introdueix un nombre vàlid de pàgines per document original.", + "collatingLoader": "S'estan ordenant les cares de l'escaneig a doble cara...", + "successMessage": "El PDF amb les cares ordenades s'ha generat correctament.", + "successGroupedMessage": "Ordenació completada. S'han generat {{fileCount}} fitxer(s) agrupat(s).", + "processErrorMessage": "No s'han pogut ordenar les cares del PDF.", + "howItWorksStep1Title": "Puja l'escaneig", + "howItWorksStep1": "Afegeix l'únic PDF que conté tots els anversos seguits de tots els reversos.", + "howItWorksStep2Title": "Defineix el punt de divisió", + "howItWorksStep2": "Marca on acaba el bloc d'anversos i indica si cal invertir l'ordre dels reversos.", + "howItWorksStep3Title": "Ordena i descarrega", + "howItWorksStep3": "Obtén un únic PDF amb les pàgines intercalades o un ZIP amb un fitxer per document original.", + "faqOddPagesQuestion": "Què passa si l'escaneig té un nombre senar de pàgines?", + "faqOddPagesAnswer": "Un escaneig a doble cara hauria de tenir un nombre parell de pàgines. Si no és així, probablement se n'ha escanejat una dues vegades o n'ha faltat alguna. L'eina t'avisa abans de processar el fitxer i afegeix al final les pàgines sense parella perquè no se'n perdi cap.", + "faqBackOrderQuestion": "Per què hauria de mantenir l'ordre original del bloc de reversos?", + "faqBackOrderAnswer": "Depèn de com hagis introduït la pila a la segona passada. Si gires tota la pila, els reversos queden en ordre invers, que és l'opció predeterminada. Si l'escàner els ha retornat en el mateix ordre que els anversos, mantén el bloc tal com està." + }, + "addAttachments": { + "name": "Afegeix fitxers adjunts", + "subtitle": "Incrusta un o més fitxers al PDF." + }, + "extractAttachments": { + "name": "Extreu fitxers adjunts", + "subtitle": "Extreu tots els fitxers incrustats dels PDF en un ZIP." + }, + "editAttachments": { + "name": "Edita els fitxers adjunts", + "subtitle": "Visualitza o elimina els fitxers adjunts del PDF." + }, + "dividePages": { + "name": "Divideix pàgines", + "subtitle": "Divideix les pàgines horitzontalment o verticalment." + }, + "addBlankPage": { + "name": "Afegeix una pàgina en blanc", + "subtitle": "Insereix una pàgina en blanc en qualsevol punt del PDF." + }, + "reversePages": { + "name": "Inverteix l'ordre de les pàgines", + "subtitle": "Inverteix l'ordre de totes les pàgines del document." + }, + "rotatePdf": { + "name": "Gira PDF", + "subtitle": "Gira les pàgines en increments de 90 graus." + }, + "rotateCustom": { + "name": "Gira amb un angle personalitzat", + "subtitle": "Gira les pàgines segons l'angle que indiquis." + }, + "nUpPdf": { + "name": "PDF amb diverses pàgines per full", + "subtitle": "Distribueix diverses pàgines en un sol full." + }, + "combineToSinglePage": { + "name": "Combina en una única pàgina", + "subtitle": "Uneix totes les pàgines en una sola pàgina contínua." + }, + "viewMetadata": { + "name": "Visualitza les metadades", + "subtitle": "Consulta les propietats ocultes del PDF." + }, + "editMetadata": { + "name": "Edita les metadades", + "subtitle": "Canvia l'autor, el títol i altres propietats." + }, + "pdfsToZip": { + "name": "PDF a ZIP", + "subtitle": "Agrupa diversos fitxers PDF en un arxiu ZIP." + }, + "comparePdfs": { + "name": "Compara PDF", + "subtitle": "Compara dos PDF de costat.", + "firstPdf": "Primer PDF", + "secondPdf": "Segon PDF", + "clickOrDrop": "Fes clic o deixa'l anar", + "page": "Pàgina", + "overlay": "Superposició", + "sideBySide": "De costat", + "flicker": "Alternança ràpida", + "syncScroll": "Sincronitza el desplaçament", + "export": "Exporta", + "exportAsPdf": "Exporta com a PDF", + "splitView": "Vista dividida", + "alternating": "Alterna", + "leftDocument": "Document esquerre", + "rightDocument": "Document dret", + "original": "Original", + "modified": "Modificat", + "searchChanges": "Cerca canvis...", + "deleted": "Suprimit", + "added": "Afegit", + "prevPage": "Pàgina anterior", + "nextPage": "Pàgina següent", + "prevChange": "Canvi anterior", + "nextChange": "Canvi següent", + "uploadTwoPdfs": "Puja dos PDF per veure'n les diferències.", + "noDifferences": "No s'han detectat diferències en aquesta pàgina.", + "noMatchingChanges": "No hi ha canvis que coincideixin amb el filtre actual.", + "pageNotExist": "La pàgina {{page}} no existeix en aquest PDF.", + "noPairedPage": "No hi ha cap pàgina corresponent en aquest costat.", + "buildingModel": "S'està creant el model de correspondència de pàgines...", + "indexingPdf": "S'està indexant el PDF {{num}}, pàgina {{page}} de {{total}}...", + "loadingComparison": "S'està carregant la comparació {{current}} de {{total}}...", + "runningOcr": "S'està executant l'OCR a la pàgina {{page}}...", + "preparingExport": "S'està preparant l'exportació del PDF...", + "renderingPage": "S'està representant la pàgina {{current}} de {{total}}...", + "exportError": "Error d'exportació", + "exportFailed": "No s'ha pogut exportar el PDF de comparació.", + "loadingFile": "S'està carregant {{name}}...", + "invalidFile": "Fitxer no vàlid", + "invalidFileMsg": "Selecciona un fitxer PDF vàlid.", + "loadError": "No s'ha pogut carregar el PDF. Pot estar malmès o protegit amb contrasenya." + }, + "posterizePdf": { + "name": "Divideix PDF en format pòster", + "subtitle": "Divideix una pàgina gran en diverses pàgines més petites." + }, + "fixPageSize": { + "name": "Uniformitza la mida de pàgina", + "subtitle": "Estableix la mateixa mida per a totes les pàgines." + }, + "linearizePdf": { + "name": "Linealitza PDF", + "subtitle": "Optimitza el PDF perquè es visualitzi ràpidament al web." + }, + "pageDimensions": { + "name": "Dimensions de pàgina", + "subtitle": "Analitza la mida, l'orientació i les unitats de les pàgines." + }, + "removeRestrictions": { + "name": "Elimina les restriccions", + "subtitle": "Elimina la protecció amb contrasenya i les restriccions de seguretat associades als PDF signats digitalment." + }, + "repairPdf": { + "name": "Repara PDF", + "subtitle": "Recupera dades de fitxers PDF corruptes o malmesos." + }, + "encryptPdf": { + "name": "Xifra PDF", + "subtitle": "Protegeix el PDF afegint-hi una contrasenya." + }, + "sanitizePdf": { + "name": "Neteja PDF", + "subtitle": "Elimina metadades, anotacions, scripts i altres elements." + }, + "decryptPdf": { + "name": "Desxifra PDF", + "subtitle": "Desbloqueja el PDF eliminant-ne la protecció amb contrasenya." + }, + "flattenPdf": { + "name": "Aplana PDF", + "subtitle": "Fes que els camps de formulari i les anotacions no es puguin editar." + }, + "removeMetadata": { + "name": "Elimina les metadades", + "subtitle": "Elimina les dades ocultes del PDF." + }, + "changePermissions": { + "name": "Canvia els permisos", + "subtitle": "Estableix o modifica els permisos d'usuari d'un PDF." + }, + "odtToPdf": { + "name": "ODT a PDF", + "subtitle": "Converteix fitxers de text OpenDocument a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers ODT", + "convertButton": "Converteix a PDF" + }, + "csvToPdf": { + "name": "CSV a PDF", + "subtitle": "Converteix fulls de càlcul CSV a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers CSV", + "convertButton": "Converteix a PDF" + }, + "rtfToPdf": { + "name": "RTF a PDF", + "subtitle": "Converteix documents de format de text enriquit (RTF) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers RTF", + "convertButton": "Converteix a PDF" + }, + "wordToPdf": { + "name": "Word a PDF", + "subtitle": "Converteix documents Word (DOCX, DOC, ODT, RTF) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers DOCX, DOC, ODT i RTF", + "convertButton": "Converteix a PDF" + }, + "excelToPdf": { + "name": "Excel a PDF", + "subtitle": "Converteix fulls de càlcul Excel (XLSX, XLS, ODS, CSV) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers XLSX, XLS, ODS i CSV", + "convertButton": "Converteix a PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint a PDF", + "subtitle": "Converteix presentacions PowerPoint (PPTX, PPT, ODP) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers PPTX, PPT i ODP", + "convertButton": "Converteix a PDF" + }, + "markdownToPdf": { + "name": "Markdown a PDF", + "subtitle": "Escriu o enganxa Markdown i exporta'l com un PDF amb un format acurat.", + "paneMarkdown": "Markdown", + "panePreview": "Previsualització", + "btnUpload": "Puja", + "btnSyncScroll": "Sincronitza el desplaçament", + "btnSettings": "Configuració", + "btnExportPdf": "Exporta el PDF", + "settingsTitle": "Configuració de Markdown", + "settingsPreset": "Configuració predefinida", + "presetDefault": "Per defecte (semblant a GFM)", + "presetCommonmark": "CommonMark (estricte)", + "presetZero": "Mínima (sense funcions)", + "settingsOptions": "Opcions de Markdown", + "optAllowHtml": "Permet etiquetes HTML", + "optBreaks": "Converteix els salts de línia a
", + "optLinkify": "Converteix automàticament els URL en enllaços", + "optTypographer": "Tipografia (cometes tipogràfiques, etc.)" + }, + "pdfBooklet": { + "name": "Fullet PDF", + "subtitle": "Reordena les pàgines per imprimir un fullet a doble cara. Plega-les i grapa-les per crear-lo.", + "howItWorks": "Com funciona:", + "step1": "Puja un fitxer PDF.", + "step2": "Les pàgines es reordenaran per imprimir el fullet.", + "step3": "Imprimeix a doble cara, gira pel costat curt, plega i grapa.", + "paperSize": "Mida del paper", + "orientation": "Orientació", + "portrait": "Vertical", + "landscape": "Horitzontal", + "pagesPerSheet": "Pàgines per full", + "createBooklet": "Crea un fullet", + "processing": "S'està processant...", + "pageCount": "Si cal, s'afegiran pàgines fins que el total sigui múltiple de 4." + }, + "xpsToPdf": { + "name": "XPS a PDF", + "subtitle": "Converteix documents XPS/OXPS a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers XPS i OXPS", + "convertButton": "Converteix a PDF" + }, + "mobiToPdf": { + "name": "MOBI a PDF", + "subtitle": "Converteix llibres electrònics MOBI a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers MOBI", + "convertButton": "Converteix a PDF" + }, + "epubToPdf": { + "name": "EPUB a PDF", + "subtitle": "Converteix llibres electrònics EPUB a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers EPUB", + "convertButton": "Converteix a PDF" + }, + "fb2ToPdf": { + "name": "FB2 a PDF", + "subtitle": "Converteix llibres electrònics FictionBook (FB2) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers FB2", + "convertButton": "Converteix a PDF" + }, + "cbzToPdf": { + "name": "CBZ a PDF", + "subtitle": "Converteix arxius de còmics (CBZ/CBR) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers CBZ i CBR", + "convertButton": "Converteix a PDF" + }, + "wpdToPdf": { + "name": "WPD a PDF", + "subtitle": "Converteix documents WordPerfect (WPD) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers WPD", + "convertButton": "Converteix a PDF" + }, + "wpsToPdf": { + "name": "WPS a PDF", + "subtitle": "Converteix documents WPS Office a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers WPS", + "convertButton": "Converteix a PDF" + }, + "xmlToPdf": { + "name": "XML a PDF", + "subtitle": "Converteix documents XML a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers XML", + "convertButton": "Converteix a PDF" + }, + "pagesToPdf": { + "name": "Pages a PDF", + "subtitle": "Converteix documents Apple Pages a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers Pages", + "convertButton": "Converteix a PDF" + }, + "odgToPdf": { + "name": "ODG a PDF", + "subtitle": "Converteix fitxers gràfics OpenDocument (ODG) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers ODG", + "convertButton": "Converteix a PDF" + }, + "odsToPdf": { + "name": "ODS a PDF", + "subtitle": "Converteix fulls de càlcul OpenDocument (ODS) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers ODS", + "convertButton": "Converteix a PDF" + }, + "odpToPdf": { + "name": "ODP a PDF", + "subtitle": "Converteix presentacions OpenDocument (ODP) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers ODP", + "convertButton": "Converteix a PDF" + }, + "pubToPdf": { + "name": "PUB a PDF", + "subtitle": "Converteix documents Microsoft Publisher (PUB) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers PUB", + "convertButton": "Converteix a PDF" + }, + "vsdToPdf": { + "name": "VSD a PDF", + "subtitle": "Converteix documents Microsoft Visio (VSD, VSDX) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers VSD i VSDX", + "convertButton": "Converteix a PDF" + }, + "psdToPdf": { + "name": "PSD a PDF", + "subtitle": "Converteix fitxers Adobe Photoshop (PSD) a PDF. Admet diversos fitxers.", + "acceptedFormats": "Fitxers PSD", + "convertButton": "Converteix a PDF" + }, + "pdfToSvg": { + "name": "PDF a SVG", + "subtitle": "Converteix cada pàgina del PDF en un gràfic vectorial escalable (SVG), amb qualitat perfecta a qualsevol mida." + }, + "extractTables": { + "name": "Extreu taules de PDF", + "subtitle": "Extreu taules de fitxers PDF i exporta-les com a CSV, JSON o Markdown." + }, + "pdfToCsv": { + "name": "PDF a CSV", + "subtitle": "Extreu taules del PDF i converteix-les a CSV." + }, + "pdfToExcel": { + "name": "PDF a Excel", + "subtitle": "Extreu taules del PDF i converteix-les a Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF a text", + "subtitle": "Extreu el text dels fitxers PDF i desa'l com a text pla (.txt). Admet diversos fitxers.", + "noteLabel": "Nota:", + "noteText": "Aquesta eina NOMÉS funciona amb PDF creats digitalment. Per als documents escanejats o basats en imatges, utilitza l'eina OCR de PDF.", + "convertButton": "Extreu el text" + }, + "digitalSignPdf": { + "name": "Signatura digital de PDF", + "pageTitle": "Signatura digital de PDF - Afegeix una signatura criptogràfica | BentoPDF", + "subtitle": "Afegeix una signatura digital criptogràfica al PDF amb certificats X.509. Admet els formats PKCS#12 (.pfx, .p12) i PEM. La clau privada no surt mai del navegador.", + "certificateSection": "Certificat", + "uploadCert": "Puja un certificat (.pfx, .p12, .pem)", + "certPassword": "Contrasenya del certificat", + "privateKeyPassword": "Contrasenya de la clau privada", + "certPasswordPlaceholder": "Introdueix la contrasenya del certificat", + "certInfo": "Informació del certificat", + "certSubject": "Titular", + "certIssuer": "Emissor", + "certValidity": "Vàlid", + "signatureDetails": "Detalls de la signatura (opcional)", + "reason": "Motiu", + "reasonPlaceholder": "p. ex., Aprovo aquest document", + "location": "Ubicació", + "locationPlaceholder": "p. ex., Barcelona, Catalunya", + "contactInfo": "Informació de contacte", + "contactPlaceholder": "p. ex., correu@exemple.cat", + "visibleSignature": "Signatura visible", + "visibleSignatureDescription": "Si s'activa, apareixerà un quadre de signatura visible al PDF. Pots pujar una imatge o un logotip, o afegir-hi text. Si no n'afegeixes cap, es mostrarà automàticament el nom del signant que consta al certificat.", + "signatureImage": "Imatge o logotip de la signatura (opcional)", + "uploadSignatureImage": "Puja una imatge (PNG, JPG, WebP)", + "signaturePreviewAlt": "Previsualització de la signatura", + "remove": "Elimina", + "page": "Pàgina", + "firstPage": "Primera pàgina", + "lastPage": "Última pàgina", + "allPages": "Totes les pàgines", + "customPage": "Pàgina personalitzada núm.", + "pageNumber": "Número de pàgina", + "xPosition": "Posició X (esquerra)", + "yPosition": "Posició Y (superior)", + "width": "Amplada", + "height": "Alçada", + "addTextOverlay": "Afegeix text superposat", + "text": "Text", + "textPlaceholder": "p. ex., Signat digitalment per Joan Garcia", + "textColor": "Color del text", + "fontSize": "Mida del tipus de lletra", + "invalidImageTitle": "Imatge no vàlida", + "invalidImageMessage": "Selecciona una imatge PNG, JPG o WebP.", + "invalidFileTitle": "Fitxer no vàlid", + "invalidFileMessage": "Selecciona un fitxer PDF.", + "invalidCertificateTitle": "Certificat no vàlid", + "invalidCertificateMessage": "Selecciona un certificat .pfx, .p12 o .pem.", + "certificateLoaded": "Certificat carregat", + "certificateUnlocked": "Certificat desbloquejat", + "failedParsePem": "No s'ha pogut interpretar el fitxer PEM", + "enterPasswordToUnlock": "Introdueix la contrasenya per desbloquejar-lo", + "invalidPasswordOrCertificate": "Contrasenya o certificat no vàlid", + "incorrectPassword": "Contrasenya incorrecta", + "failedParseCertificate": "No s'ha pogut interpretar el certificat", + "missingDataTitle": "Falten dades", + "missingDataMessage": "Puja un PDF i un certificat vàlid.", + "applyingSignature": "S'està aplicant la signatura digital...", + "signingFailedTitle": "No s'ha pogut signar", + "chainFetchFailed": "No s'ha pogut obtenir la cadena de certificats. Pot ser per problemes de xarxa o perquè el servidor intermediari de certificats no està disponible. Comprova la connexió a Internet i torna-ho a provar. Si el problema continua, contacta amb el servei d'assistència.", + "failedToSignPdf": "No s'ha pogut signar el PDF: {{message}}", + "defaultSignatureText": "Signat digitalment per {{signer}}\n{{date}}", + "pageCount": "{{count}} pàgines", + "howToStep1Title": "Puja un PDF", + "howToStep1Text": "Selecciona el document PDF que vols signar", + "howToStep2Title": "Puja el certificat", + "howToStep2Text": "Proporciona el fitxer de certificat .pfx o .p12 i la contrasenya", + "howToStep3Title": "Signa i descarrega", + "howToStep3Text": "Aplica la signatura digital i descarrega el PDF signat", + "relatedEncryptDesc": "Protegeix el PDF amb contrasenya", + "relatedDecryptDesc": "Elimina la contrasenya del PDF", + "relatedFlattenDesc": "Fes que les anotacions siguin permanents", + "relatedSanitizeDesc": "Elimina les dades ocultes", + "relatedPermissionsDesc": "Estableix els permisos del PDF", + "faqWhatIsQuestion": "Què és una signatura digital?", + "faqWhatIsAnswer": "Una signatura digital és un mecanisme criptogràfic que verifica l'autenticitat i la integritat d'un document. A diferència d'una signatura dibuixada, utilitza criptografia de clau pública (certificats X.509) per acreditar la identitat del signant.", + "faqFormatsQuestion": "Quins formats de certificat s'admeten?", + "faqFormatsAnswer": "Admetem certificats en format PKCS#12 (.pfx, .p12), que contenen el certificat i la clau privada en un únic fitxer xifrat. Habitualment els emeten entitats de certificació.", + "faqPrivateKeyQuestion": "La meva clau privada és segura?", + "faqPrivateKeyAnswer": "Sí! Tot el processament es fa íntegrament al navegador. El certificat, la clau privada i els fitxers PDF no surten del dispositiu ni s'envien a cap servidor.", + "faqValidReadersQuestion": "La signatura serà vàlida als lectors de PDF?", + "faqValidReadersAnswer": "Sí, l'eina crea signatures separades PKCS#7 estàndard que reconeixen els principals visors de PDF. Per validar-les completament, utilitza un certificat d'una entitat de certificació de confiança.", + "applySignature": "Aplica la signatura digital", + "successMessage": "El PDF s'ha signat correctament! Pots verificar la signatura amb qualsevol lector de PDF.", + "internetRequiredTitle": "Cal connexió a Internet", + "internetRequiredDescription": "Per validar la signatura, pot ser necessari obtenir la cadena de certificats del servidor de l'emissor." + }, + "validateSignaturePdf": { + "name": "Valida la signatura d'un PDF", + "pageTitle": "Valida la signatura d'un PDF - Verifica signatures digitals | BentoPDF", + "subtitle": "Verifica les signatures digitals dels fitxers PDF. Comprova la validesa del certificat, consulta els detalls del signant i confirma la integritat del document. Tot el processament es fa al navegador." + }, + "timestampPdf": { + "name": "Segell de temps de PDF", + "pageTitle": "Segell de temps de PDF gratuït en línia - Segell de temps de document RFC 3161 | BentoPDF", + "subtitle": "Afegeix un segell de temps de document RFC 3161 al PDF mitjançant un servidor de segellament de temps de confiança (TSA). Demostra que el document existia en un moment determinat. No cal cap certificat.", + "tsaSectionTitle": "Servidor de segellament de temps (TSA)", + "selectTsa": "Selecciona un servidor TSA", + "applyTimestamp": "Aplica el segell de temps", + "successMessage": "El segell de temps s'ha aplicat correctament al PDF! El pots verificar amb qualsevol lector de PDF." + }, + "emailToPdf": { + "name": "Correu electrònic a PDF", + "subtitle": "Converteix fitxers de correu electrònic (EML, MSG) a PDF. Admet exportacions d'Outlook i formats de correu estàndard.", + "acceptedFormats": "Fitxers EML i MSG", + "convertButton": "Converteix a PDF" + }, + "fontToOutline": { + "name": "Converteix tipus de lletra en contorns", + "subtitle": "Converteix tots els tipus de lletra en contorns vectorials per garantir una visualització uniforme a tots els dispositius." + }, + "deskewPdf": { + "name": "Redreça PDF", + "subtitle": "Redreça automàticament les pàgines escanejades inclinades amb OpenCV." + }, + "pdfToWord": { + "name": "PDF a Word", + "subtitle": "Converteix fitxers PDF en documents Word editables." + }, + "extractImages": { + "name": "Extreu imatges", + "subtitle": "Extreu totes les imatges incrustades dels fitxers PDF." + }, + "pdfToMarkdown": { + "name": "PDF a Markdown", + "subtitle": "Converteix el text i les taules del PDF al format Markdown." + }, + "preparePdfForAi": { + "name": "Prepara PDF per a IA", + "subtitle": "Extreu el contingut del PDF com a JSON LlamaIndex per a fluxos RAG/LLM." + }, + "pdfOcg": { + "name": "Capes OCG del PDF", + "subtitle": "Visualitza, activa o desactiva, afegeix i suprimeix capes OCG del PDF." + }, + "pdfToPdfa": { + "name": "PDF a PDF/A", + "subtitle": "Converteix PDF a PDF/A per a l'arxivament a llarg termini." + }, + "rasterizePdf": { + "name": "Rasteritza PDF", + "subtitle": "Converteix el PDF en un PDF basat en imatges. Aplana les capes i elimina el text seleccionable." + }, + "pdfWorkflow": { + "name": "Creador de fluxos de treball PDF", + "subtitle": "Crea fluxos de processament PDF personalitzats amb un editor visual de nodes.", + "nodes": "Nodes", + "searchNodes": "Cerca nodes...", + "run": "Executa", + "clear": "Neteja", + "save": "Desa", + "load": "Carrega", + "export": "Exporta", + "import": "Importa", + "ready": "A punt", + "settings": "Configuració", + "processing": "S'està processant...", + "saveTemplate": "Desa la plantilla", + "templateName": "Nom de la plantilla", + "templatePlaceholder": "p. ex., Flux de treball de factures", + "cancel": "Cancel·la", + "loadTemplate": "Carrega la plantilla", + "noTemplates": "Encara no hi ha cap plantilla desada.", + "ok": "D'acord", + "workflowCompleted": "Flux de treball completat", + "errorDuringExecution": "Error durant l'execució", + "addNodeError": "Afegeix com a mínim un node per executar el flux de treball.", + "needInputOutput": "El flux de treball necessita com a mínim un node d'entrada i un de sortida.", + "enterName": "Introdueix un nom.", + "templateExists": "Ja existeix una plantilla amb aquest nom.", + "templateSaved": "S'ha desat la plantilla «{{name}}».", + "templateLoaded": "S'ha carregat la plantilla «{{name}}».", + "failedLoadTemplate": "No s'ha pogut carregar la plantilla.", + "noSettings": "Aquest node no té cap opció configurable.", + "advancedSettings": "Configuració avançada", + "specialNodes": { + "pdfInput": { + "name": "Entrada de PDF", + "description": "Puja un o més fitxers PDF" + }, + "download": { + "name": "Descarrega", + "description": "Descarrega automàticament com a PDF o ZIP" + } + }, + "errors": { + "noPdfsConnected": "No hi ha cap PDF connectat al node {{node}}", + "noPdfInputs": "No hi ha cap PDF a l'entrada", + "expectedPdfInput": "S'esperava una entrada PDF", + "failedToRenderPage": "No s'ha pogut representar la pàgina {{page}}", + "failedToGetCanvasContext": "No s'ha pogut obtenir el context del llenç per a la pàgina {{page}}", + "failedToRenderPageToImage": "No s'ha pogut representar la pàgina {{page}} com a imatge", + "noPagesProcessed": "No s'ha processat cap pàgina", + "cpdfNotConfigured": "CoherentPDF no està configurat. Configura'l als paràmetres avançats.", + "workerError": "Error del procés de treball: {{message}}", + "alternateMergeWorkerError": "Error del procés de fusió alternada: {{message}}", + "noPdfFilesUploaded": "No s'ha pujat cap PDF al node Entrada de PDF", + "failedToLoadFile": "No s'ha pogut carregar «{{file}}»; pot ser que estigui malmès", + "noImagesUploaded": "No s'ha pujat cap imatge al node Entrada d'imatges", + "noWordDocsUploaded": "No s'ha pujat cap document al node Entrada de Word", + "noExcelUploaded": "No s'ha pujat cap full de càlcul al node Entrada d'Excel", + "noPowerPointUploaded": "No s'ha pujat cap presentació al node Entrada de PowerPoint", + "noCbzUploaded": "No s'ha pujat cap arxiu de còmics al node Entrada de CBZ", + "noEmailUploaded": "No s'ha pujat cap fitxer de correu al node Entrada de correu electrònic", + "noEpubUploaded": "No s'ha pujat cap fitxer EPUB al node Entrada d'EPUB", + "noFb2Uploaded": "No s'ha pujat cap fitxer FB2 al node Entrada de FB2", + "noJsonUploaded": "No s'ha pujat cap fitxer JSON al node Entrada de JSON", + "noMarkdownUploaded": "No s'ha pujat cap fitxer Markdown al node Entrada de Markdown", + "noMobiUploaded": "No s'ha pujat cap fitxer MOBI al node Entrada de MOBI", + "noOdgUploaded": "No s'ha pujat cap fitxer ODG al node Entrada d'ODG", + "noPagesUploaded": "No s'ha pujat cap fitxer Pages al node Entrada de Pages", + "noPubUploaded": "No s'ha pujat cap fitxer PUB al node Entrada de PUB", + "noSvgUploaded": "No s'ha pujat cap fitxer SVG al node Entrada de SVG", + "noTextUploaded": "No s'ha pujat cap fitxer de text al node Entrada de text", + "noVsdUploaded": "No s'ha pujat cap fitxer Visio al node Entrada de VSD", + "noWpdUploaded": "No s'ha pujat cap fitxer WPD al node Entrada de WPD", + "noWpsUploaded": "No s'ha pujat cap fitxer WPS al node Entrada de WPS", + "noXmlUploaded": "No s'ha pujat cap fitxer XML al node Entrada d'XML", + "noXpsUploaded": "No s'ha pujat cap fitxer XPS al node Entrada d'XPS", + "alternateMergeNeedsTwo": "La fusió alternada necessita com a mínim dos PDF connectats a l'entrada", + "cropMarginsExceed": "Els marges de retall superen les dimensions de la pàgina. Redueix-ne els valors.", + "digitalSignNoCertificate": "No s'ha carregat cap certificat al node de signatura digital", + "encryptUserPasswordRequired": "Cal una contrasenya d'usuari per xifrar el fitxer", + "extractImagesNoneFound": "No s'ha trobat cap imatge als PDF connectats", + "invertColorsFailedReadImage": "No s'han pogut llegir les dades de la imatge", + "overlayRequiresPdf": "El node de superposició necessita un PDF per superposar.", + "pdfToCsvNoTables": "No s'ha trobat cap taula al PDF", + "pdfToXlsxNoTables": "No s'ha trobat cap taula a {{file}}", + "posterizeFailedTileCanvas": "No s'ha pogut obtenir el context del llenç de mosaic", + "redactNoText": "Ocultació: no s'ha especificat cap text per ocultar", + "removeBlankPagesAllBlank": "Totes les pàgines són en blanc", + "svgFailedToConvert": "No s'ha pogut convertir l'SVG", + "svgFailedToLoad": "No s'ha pogut carregar l'SVG", + "timestampFailed": "No s'ha pogut aplicar el segell de temps amb el servidor TSA {{tsa}}: {{message}}", + "noOutputNodes": "No s'ha trobat cap node de sortida. Afegeix un node Descarrega per completar el flux de treball.", + "circularDependency": "S'ha detectat una dependència circular al flux de treball. Elimina els bucles entre nodes.", + "encryptOrdering": "El node Xifra envia dades a «{{target}}», que pot fallar amb dades xifrades. Mou Xifra just abans del node de sortida.", + "invalidWorkflowFile": "Fitxer de flux de treball no vàlid: falta la llista de nodes o de connexions.", + "storageQuotaExceeded": "No s'ha pogut desar el flux de treball: s'ha superat la quota d'emmagatzematge. Prova d'eliminar plantilles antigues.", + "failedToImport": "No s'ha pogut importar el flux de treball: {{message}}" + } + }, + "wasmSettings": { + "title": "Configuració de funcions avançades", + "description": "Configura mòduls de processament externs per activar funcions avançades de PDF. Són opcionals i tenen llicències independents.", + "status": { + "testing": "S'està provant...", + "customOverride": "Configuració personalitzada", + "preConfigured": "Preconfigurat", + "notConfigured": "No configurat" + }, + "preconfiguredTitle": "Preconfigurat i a punt per utilitzar.", + "preconfiguredDescription": "Els mòduls de processament avançat es carreguen automàticament des d'una CDN. Pots substituir els URL següents si necessites una font personalitzada (p. ex., en instal·lacions sense accés a Internet o allotjades al teu servidor).", + "usingDefault": "Valor predeterminat: {{url}}", + "pymupdfSubtitle": "Motor de processament de documents", + "pymupdfFeatures": "Permet: PDF a text, Markdown, SVG, DOCX, Excel • Extracció d'imatges i taules • Conversió de formats", + "testButton": "Prova", + "recommendedLabel": "Recomanat:", + "copyTitle": "Copia al porta-retalls", + "ghostscriptSubtitle": "Motor de conversió a PDF/A", + "ghostscriptFeatures": "Permet: conversió a PDF/A-1b, PDF/A-2b i PDF/A-3b • Conversió de tipus de lletra en contorns", + "cpdfSubtitle": "Motor de marcadors i metadades", + "cpdfFeatures": "Permet: divisió per marcadors • Edició de marcadors • Metadades PDF", + "saveConfiguration": "Desa la configuració", + "savingConfiguration": "S'està desant la configuració...", + "savedTitle": "Desat", + "savedMessage": "La configuració s'ha desat correctament!", + "errorTitle": "Error", + "failedSave": "No s'ha pogut desar la configuració: {{message}}", + "emptyUrlTitle": "URL buit", + "emptyUrlMessage": "Introdueix un URL per fer la prova.", + "testSuccessTitle": "Correcte", + "connectionSuccess": "S'ha establert correctament la connexió amb {{package}}!", + "connectionFailedTitle": "Error de connexió", + "connectionFailedMessage": "No s'ha pogut connectar a l'URL.", + "resetDefaults": "Restableix els valors predeterminats", + "resetTitle": "Restableix", + "resetWithDefaults": "S'han eliminat les modificacions personalitzades. Els valors preconfigurats tornen a estar actius.", + "resetNoDefaults": "S'han eliminat totes les configuracions i els mòduls emmagatzemats a la memòria cau.", + "licenseTitle": "Avís de llicència:", + "licenseDescription": "Els mòduls externs (PyMuPDF, Ghostscript, CoherentPDF) es distribueixen amb llicències AGPL-3.0 o altres llicències copyleft similars. Si configures i utilitzes aquests mòduls, acceptes les condicions de les llicències corresponents. BentoPDF és compatible amb qualsevol implementació Ghostscript WASM o PyMuPDF WASM que segueixi la interfície prevista." + }, + "pdfOverlay": { + "name": "Superposa PDF", + "subtitle": "Superposa pàgines d'un PDF per damunt o per sota de les d'un altre.", + "basePdfLabel": "PDF de base", + "uploadBasePdf": "Puja el PDF de base", + "overlayPdfLabel": "PDF per superposar / posar al fons", + "uploadOverlayPdf": "Puja el PDF per superposar o posar al fons", + "modeLabel": "Mode", + "overlayOption": "Superposa (damunt de les pàgines)", + "underlayOption": "Posa al fons (darrere de les pàgines)", + "pageRangeLabel": "Aplica a les pàgines (opcional)", + "pageRangeHint": "Utilitza «z» per indicar l'última pàgina. Deixa el camp buit per aplicar-ho a totes les pàgines.", + "repeatLabel": "Repeteix les pàgines superposades o de fons si el PDF de base és més llarg", + "processButton": "Aplica la superposició / el fons", + "howItWorksUploadTitle": "Puja dos PDF", + "howItWorksUploadDescription": "Puja el PDF de base i el PDF que vols superposar o posar al fons.", + "howItWorksModeTitle": "Tria el mode", + "howItWorksModeDescription": "Tria superposar (a sobre) o posar al fons (a sota). També pots indicar un interval de pàgines i repetir-les.", + "howItWorksDownloadTitle": "Descarrega", + "howItWorksDownloadDescription": "Descarrega el PDF amb la superposició o el fons aplicat.", + "faqDifferenceQuestion": "Quina diferència hi ha entre superposar i posar al fons?", + "faqDifferenceAnswer": "La superposició col·loca les pàgines damunt del PDF de base (com una marca d'aigua). L'opció de fons les col·loca darrere del PDF de base (com un paper de carta o un fons).", + "faqSinglePageQuestion": "Puc utilitzar un PDF d'una sola pàgina com a superposició per a totes les pàgines?", + "faqSinglePageAnswer": "Sí! Activa l'opció «Repeteix» i les pàgines del PDF superposat es repetiran sobre totes les pàgines del PDF de base. Una marca d'aigua d'una sola pàgina s'aplicarà a totes les pàgines.", + "faqPrivacyQuestion": "Els meus fitxers són privats i segurs?", + "faqPrivacyAnswer": "I tant! Tot el processament es fa al navegador. Els fitxers no surten mai del dispositiu, de manera que es garanteix la privadesa." + }, + "unlockPdf": { + "name": "Desbloqueja PDF", + "subtitle": "Elimina la contrasenya d'un PDF perquè es pugui obrir lliurement. Cal conèixer la contrasenya actual." + }, + "protectPdf": { + "name": "Protegeix PDF", + "subtitle": "Protegeix un PDF amb contrasenya mitjançant un xifratge AES robust." + } +} diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index 82747619..e0057980 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -28,6 +28,7 @@ export const supportedLanguages = [ 'ja', 'uk', 'sk', + 'ca', ] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; @@ -54,6 +55,7 @@ export const languageNames: Record = { ja: '日本語', uk: 'Українська', sk: 'Slovenčina', + ca: 'Català', }; export const getLanguageFromUrl = (): SupportedLanguage => { @@ -69,7 +71,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => { } const langMatch = path.match( - /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(?:\/|$)/ + /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca)(?:\/|$)/ ); if ( langMatch && @@ -166,7 +168,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => { let pagePathWithoutLang = relativePath; const langPrefixMatch = relativePath.match( - /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(\/.*)?$/ + /^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca)(\/.*)?$/ ); if (langPrefixMatch) { pagePathWithoutLang = langPrefixMatch[2] || '/'; @@ -261,7 +263,7 @@ export const rewriteLinks = (): void => { } const langPrefixRegex = new RegExp( - `^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(/|$)` + `^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca)(/|$)` ); if (langPrefixRegex.test(href)) { return; diff --git a/vite.config.ts b/vite.config.ts index f62b3647..3e98066e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,6 +49,7 @@ const SUPPORTED_LANGUAGES = [ 'ja', 'uk', 'sk', + 'ca', ] as const; const LANG_REGEX = new RegExp( `^/(${SUPPORTED_LANGUAGES.join('|')})(?:/(.*))?$` From 5992cae40b689a41c858ed1dbdb7cebb397f7322 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Thu, 22 Oct 2026 12:47:54 +0530 Subject: [PATCH 05/10] Sync Farsi translation with main --- public/locales/fa/common.json | 7 +++-- public/locales/fa/tools.json | 50 ++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index f7697cde..4a1d3ac3 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -239,7 +239,9 @@ "legal": "حقوقی", "termsAndConditions": "شرایط و ضوابط", "privacyPolicy": "سیاست حفظ حریم خصوصی", - "followUs": "ما را دنبال کنید" + "followUs": "ما را دنبال کنید", + "privacyShort": "حریم خصوصی", + "termsShort": "شرایط" }, "merge": { "title": "ادغام PDFها", @@ -286,7 +288,8 @@ "noCompromises": "بدون هیچ مصالحه‌ای." }, "mission": { - "title": "مأموریت ما", + "title": "مأموریت", + "titleDim": "ما", "description": "ارائهٔ جامع‌ترین جعبه‌ابزار PDF که به حریم خصوصی شما احترام می‌گذارد و هرگز درخواست پرداخت نمی‌کند. ما باور داریم ابزارهای ضروری اسناد باید برای همه، در همه جا و بدون مانع در دسترس باشند." }, "philosophy": { diff --git a/public/locales/fa/tools.json b/public/locales/fa/tools.json index 9bd4e85f..d38580b4 100644 --- a/public/locales/fa/tools.json +++ b/public/locales/fa/tools.json @@ -20,7 +20,21 @@ "subtitle": "چند PDF را در یک فایل ترکیب کنید. نشانک‌ها حفظ می‌شوند.", "retainPageLabels": "حفظ برچسب‌های اصلی صفحات هر فایل", "retainPageLabelsHint": "خاموش (پیش‌فرض): PDF ادغام‌شده از شماره‌گذاری طبیعی ۱ تا N استفاده می‌کند. روشن: برچسب‌های اصلی صفحات هر فایل حفظ می‌شوند (ممکن است برچسب‌های تکراری پدید آید).", - "removeDuplicateFonts": "حذف قلم‌های تکراری" + "removeDuplicateFonts": "حذف قلم‌های تکراری", + "alert": { + "thumbnailError": "ترسیم بندانگشتی صفحات ناموفق بود", + "noSelection": "هیچ فایل یا صفحه‌ای برای ادغام انتخاب نشده است.", + "mergeSuccess": "PDFها با موفقیت ادغام شدند!", + "mergeFailed": "ادغام PDFها ناموفق بود.", + "mergeError": "ادغام PDFها ناموفق بود. لطفاً بررسی کنید که همهٔ فایل‌ها معتبر باشند و گذرواژه نداشته باشند.", + "workerError": "هنگام ادغام خطای غیرمنتظره‌ای رخ داد.", + "loadError": "بارگذاری یک یا چند فایل PDF ناموفق بود" + }, + "loader": { + "renderingPreviews": "در حال ترسیم پیش‌نمایش صفحات…", + "merging": "در حال ادغام PDFها…", + "loadingDocuments": "در حال بارگذاری اسناد PDF…" + } }, "splitPdf": { "name": "تقسیم PDF", @@ -28,29 +42,7 @@ }, "compressPdf": { "name": "فشرده‌سازی PDF", - "subtitle": "حجم فایل PDF خود را کاهش دهید.", - "algorithmLabel": "الگوریتم فشرده‌سازی", - "condense": "Condense (پیشنهادی)", - "photon": "Photon (برای PDFهای پرتصویر)", - "condenseInfo": "Condense از فشرده‌سازی پیشرفته بهره می‌گیرد: حذف داده‌های زائد، بهینه‌سازی تصاویر و کاهش قلم‌ها. مناسب برای بیشتر PDFها.", - "photonInfo": "Photon صفحات را به تصویر تبدیل می‌کند. برای PDFهای پرتصویر یا اسکن‌شده به کار می‌رود.", - "photonWarning": "هشدار: متن دیگر قابل انتخاب نخواهد بود و پیوندها از کار می‌افتند.", - "levelLabel": "سطح فشرده‌سازی", - "light": "سبک (حفظ کیفیت)", - "balanced": "متعادل (پیشنهادی)", - "aggressive": "تهاجمی (فایل‌های کوچک‌تر)", - "extreme": "بیشینه (حداکثر فشرده‌سازی)", - "grayscale": "تبدیل به خاکستری", - "grayscaleHint": "با حذف اطلاعات رنگ، حجم فایل را کاهش می‌دهد", - "customSettings": "تنظیمات سفارشی", - "customSettingsHint": "تنظیم دقیق پارامترهای فشرده‌سازی:", - "outputQuality": "کیفیت خروجی", - "resizeImagesTo": "تغییر اندازهٔ تصاویر به", - "onlyProcessAbove": "فقط پردازش موارد بالاتر از", - "removeMetadata": "حذف فراداده", - "subsetFonts": "کاهش قلم‌ها (حذف نویسه‌های بلااستفاده)", - "removeThumbnails": "حذف بندانگشتی‌های تعبیه‌شده", - "compressButton": "فشرده‌سازی PDF" + "subtitle": "حجم فایل PDF خود را کاهش دهید." }, "pdfEditor": { "name": "ویرایشگر PDF", @@ -61,6 +53,8 @@ "subtitle": "روی هر پاراگراف کلیک کنید و آن را در جای خود، همراه با بازچینش زنده، قلم‌ها و قالب‌بندی، ویرایش کنید.", "saved": "PDF با موفقیت ذخیره شد! ({{size}} کیلوبایت)", "failedToLoad": "بارگذاری ویرایشگر متن PDF ناموفق بود.", + "loadingFonts": "در حال بارگذاری قلم‌ها…", + "fontsUnavailable": "قلمی که برای برخی نویسه‌ها لازم است دانلود نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید، وگرنه ممکن است آن نویسه‌ها در PDF ذخیره‌شده درست نمایش داده نشوند.", "howToUpload": "بارگذاری PDF", "howToClick": "کلیک روی یک پاراگراف", "howToRestyle": "تغییر قالب هر عنصر", @@ -1313,5 +1307,13 @@ "faqSinglePageAnswer": "بله! گزینهٔ «تکرار» را فعال کنید تا صفحات PDF روی‌گذاری در همهٔ صفحات پایه تکرار شوند. یک واترمارک تک‌صفحه‌ای بر همهٔ صفحات اعمال خواهد شد.", "faqPrivacyQuestion": "آیا فایل‌های من خصوصی و ایمن هستند؟", "faqPrivacyAnswer": "کاملاً! تمام پردازش‌ها در مرورگر شما انجام می‌شود. فایل‌های شما هرگز از دستگاهتان خارج نمی‌شوند و حریم خصوصی کامل تضمین می‌شود." + }, + "unlockPdf": { + "name": "باز کردن قفل PDF", + "subtitle": "گذرواژهٔ PDF را بردارید تا بدون آن باز شود. به گذرواژهٔ فعلی نیاز دارد." + }, + "protectPdf": { + "name": "محافظت از PDF", + "subtitle": "با رمزگذاری قوی AES، از PDF با گذرواژه محافظت کنید." } } From 4d3e8b964fdbc95e6a5607b543fa23403d9d4a86 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Thu, 22 Oct 2026 13:34:35 +0530 Subject: [PATCH 06/10] fix: fit wide tables when printing, keep editor panes from collapsing, rename Upload to Select in Markdown to PDF --- public/locales/ar/tools.json | 2 +- public/locales/be/tools.json | 2 +- public/locales/ca/tools.json | 2 +- public/locales/da/tools.json | 2 +- public/locales/de/tools.json | 2 +- public/locales/en/tools.json | 2 +- public/locales/es/tools.json | 2 +- public/locales/fa/tools.json | 2 +- public/locales/fr/tools.json | 2 +- public/locales/id/tools.json | 2 +- public/locales/it/tools.json | 2 +- public/locales/ja/tools.json | 2 +- public/locales/ko/tools.json | 2 +- public/locales/nl/tools.json | 2 +- public/locales/pl/tools.json | 2 +- public/locales/pt/tools.json | 2 +- public/locales/ru/tools.json | 2 +- public/locales/sk/tools.json | 2 +- public/locales/sv/tools.json | 2 +- public/locales/tr/tools.json | 2 +- public/locales/uk/tools.json | 2 +- public/locales/vi/tools.json | 2 +- public/locales/zh-TW/tools.json | 2 +- public/locales/zh/tools.json | 2 +- src/css/markdown-editor.css | 47 +++++++++++++++++++++++++-------- src/js/utils/markdown-editor.ts | 32 +++++++++++++++++++++- 26 files changed, 91 insertions(+), 36 deletions(-) diff --git a/public/locales/ar/tools.json b/public/locales/ar/tools.json index bbe24e44..12f7c8aa 100644 --- a/public/locales/ar/tools.json +++ b/public/locales/ar/tools.json @@ -840,7 +840,7 @@ "subtitle": "اكتب أو الصق Markdown وصدّره كـ PDF منسق بشكل جميل.", "paneMarkdown": "Markdown", "panePreview": "معاينة", - "btnUpload": "رفع", + "btnUpload": "اختيار", "btnSyncScroll": "مزامنة التمرير", "btnSettings": "الإعدادات", "btnExportPdf": "تصدير PDF", diff --git a/public/locales/be/tools.json b/public/locales/be/tools.json index 1aa14b54..1fd40b3f 100644 --- a/public/locales/be/tools.json +++ b/public/locales/be/tools.json @@ -842,7 +842,7 @@ "subtitle": "Напісаць або ўставіць Markdown і экспартаваць яго ў прыгожа аформлены PDF.", "paneMarkdown": "Markdown", "panePreview": "Папярэдні прагляд", - "btnUpload": "Запампаваць", + "btnUpload": "Выбраць", "btnSyncScroll": "Сінхранізацыя пракруткі", "btnSettings": "Налады", "btnExportPdf": "Экспартаваць PDF", diff --git a/public/locales/ca/tools.json b/public/locales/ca/tools.json index 6cf611bf..5efa0cd7 100644 --- a/public/locales/ca/tools.json +++ b/public/locales/ca/tools.json @@ -864,7 +864,7 @@ "subtitle": "Escriu o enganxa Markdown i exporta'l com un PDF amb un format acurat.", "paneMarkdown": "Markdown", "panePreview": "Previsualització", - "btnUpload": "Puja", + "btnUpload": "Selecciona", "btnSyncScroll": "Sincronitza el desplaçament", "btnSettings": "Configuració", "btnExportPdf": "Exporta el PDF", diff --git a/public/locales/da/tools.json b/public/locales/da/tools.json index 31791d3e..4ea3503f 100644 --- a/public/locales/da/tools.json +++ b/public/locales/da/tools.json @@ -840,7 +840,7 @@ "subtitle": "Skriv eller indsæt Markdown og eksportér som en flot formateret PDF.", "paneMarkdown": "Markdown", "panePreview": "Forhåndsvisning", - "btnUpload": "Upload", + "btnUpload": "Vælg", "btnSyncScroll": "Synkron rulning", "btnSettings": "Indstillinger", "btnExportPdf": "Eksportér PDF", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index dda99cf7..8131602a 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -840,7 +840,7 @@ "subtitle": "Schreiben oder fügen Sie Markdown ein und exportieren Sie es als schön formatiertes PDF.", "paneMarkdown": "Markdown", "panePreview": "Vorschau", - "btnUpload": "Hochladen", + "btnUpload": "Auswählen", "btnSyncScroll": "Sync-Scrollen", "btnSettings": "Einstellungen", "btnExportPdf": "PDF exportieren", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 79aee3ec..16128407 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -856,7 +856,7 @@ "subtitle": "Write or paste Markdown and export it as a beautifully formatted PDF.", "paneMarkdown": "Markdown", "panePreview": "Preview", - "btnUpload": "Upload", + "btnUpload": "Select", "btnSyncScroll": "Sync Scroll", "btnSettings": "Settings", "btnExportPdf": "Export PDF", diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json index ea20b951..680d97f5 100644 --- a/public/locales/es/tools.json +++ b/public/locales/es/tools.json @@ -840,7 +840,7 @@ "subtitle": "Escribe o pega Markdown y expórtalo como un PDF bellamente formateado.", "paneMarkdown": "Markdown", "panePreview": "Vista Previa", - "btnUpload": "Cargar", + "btnUpload": "Seleccionar", "btnSyncScroll": "Sincronizar Desplazamiento", "btnSettings": "Configuración", "btnExportPdf": "Exportar PDF", diff --git a/public/locales/fa/tools.json b/public/locales/fa/tools.json index d38580b4..9f866770 100644 --- a/public/locales/fa/tools.json +++ b/public/locales/fa/tools.json @@ -856,7 +856,7 @@ "subtitle": "Markdown بنویسید یا بچسبانید و آن را به‌صورت یک PDF زیبا برون‌ریزی کنید.", "paneMarkdown": "Markdown", "panePreview": "پیش‌نمایش", - "btnUpload": "بارگذاری", + "btnUpload": "انتخاب", "btnSyncScroll": "پیمایش هم‌زمان", "btnSettings": "تنظیمات", "btnExportPdf": "برون‌ریزی PDF", diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index 2cf90570..93e1dcff 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -855,7 +855,7 @@ "subtitle": "Écrire ou coller du Markdown et l’exporter en PDF avec une mise en forme soignée.", "paneMarkdown": "Markdown", "panePreview": "Aperçu", - "btnUpload": "Téléverser", + "btnUpload": "Sélectionner", "btnSyncScroll": "Synchroniser le défilement", "btnSettings": "Paramètres", "btnExportPdf": "Exporter en PDF", diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index af798e0b..d7648eac 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -840,7 +840,7 @@ "subtitle": "Tulis atau tempel Markdown dan ekspor sebagai PDF yang diformat dengan indah.", "paneMarkdown": "Markdown", "panePreview": "Pratinjau", - "btnUpload": "Unggah", + "btnUpload": "Pilih", "btnSyncScroll": "Sinkron Gulir", "btnSettings": "Pengaturan", "btnExportPdf": "Ekspor PDF", diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 7a4c9f08..e26441f5 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -840,7 +840,7 @@ "subtitle": "Scrivi o incolla Markdown ed esportalo come un PDF ben formattato.", "paneMarkdown": "Markdown", "panePreview": "Anteprima", - "btnUpload": "Carica", + "btnUpload": "Seleziona", "btnSyncScroll": "Sincronizza scorrimento", "btnSettings": "Impostazioni", "btnExportPdf": "Esporta PDF", diff --git a/public/locales/ja/tools.json b/public/locales/ja/tools.json index f7bcc395..47621da8 100644 --- a/public/locales/ja/tools.json +++ b/public/locales/ja/tools.json @@ -840,7 +840,7 @@ "subtitle": "Markdownを記述または貼り付けて、見栄えの良いPDFとしてエクスポートします。", "paneMarkdown": "マークダウン", "panePreview": "プレビュー", - "btnUpload": "アップロード", + "btnUpload": "選択", "btnSyncScroll": "同期スクロール", "btnSettings": "設定", "btnExportPdf": "PDFをエクスポート", diff --git a/public/locales/ko/tools.json b/public/locales/ko/tools.json index a52649ca..d3d36196 100644 --- a/public/locales/ko/tools.json +++ b/public/locales/ko/tools.json @@ -840,7 +840,7 @@ "subtitle": "Markdown을 작성하거나 붙여넣고 깔끔하게 서식이 적용된 PDF로 내보냅니다.", "paneMarkdown": "Markdown", "panePreview": "미리보기", - "btnUpload": "업로드", + "btnUpload": "선택", "btnSyncScroll": "스크롤 동기화", "btnSettings": "설정", "btnExportPdf": "PDF 내보내기", diff --git a/public/locales/nl/tools.json b/public/locales/nl/tools.json index ce5adb42..1059a8f4 100644 --- a/public/locales/nl/tools.json +++ b/public/locales/nl/tools.json @@ -840,7 +840,7 @@ "subtitle": "Schrijf of plak Markdown en zet het om in een opgemaakte PDF.", "paneMarkdown": "Markdown", "panePreview": "Voorbeeld", - "btnUpload": "Laden", + "btnUpload": "Selecteren", "btnSyncScroll": "Synchroon bladeren", "btnSettings": "Instellingen", "btnExportPdf": "PDF exporteren", diff --git a/public/locales/pl/tools.json b/public/locales/pl/tools.json index 4dabba06..414b2bd5 100644 --- a/public/locales/pl/tools.json +++ b/public/locales/pl/tools.json @@ -677,7 +677,7 @@ "subtitle": "Napisz lub wklej Markdown i wyeksportuj jako pięknie sformatowany PDF.", "paneMarkdown": "Markdown", "panePreview": "Podgląd", - "btnUpload": "Prześlij", + "btnUpload": "Wybierz", "btnSyncScroll": "Zsynchronizowane przewijanie", "btnSettings": "Ustawienia", "btnExportPdf": "Eksportuj PDF", diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json index 0482a6dd..0fa23d79 100644 --- a/public/locales/pt/tools.json +++ b/public/locales/pt/tools.json @@ -840,7 +840,7 @@ "subtitle": "Escreva ou cole Markdown e exporte como um belo formato PDF.", "paneMarkdown": "Markdown", "panePreview": "Pré-visualizar", - "btnUpload": "Upload", + "btnUpload": "Selecionar", "btnSyncScroll": "Sincronizar Scroll", "btnSettings": "Configurações", "btnExportPdf": "Exportar PDF", diff --git a/public/locales/ru/tools.json b/public/locales/ru/tools.json index 19c38a10..0f7e931d 100644 --- a/public/locales/ru/tools.json +++ b/public/locales/ru/tools.json @@ -840,7 +840,7 @@ "subtitle": "Написать или вставить Markdown, чтобы экспортировать его в виде отформатированного PDF.", "paneMarkdown": "Markdown", "panePreview": "Предварительный просмотр", - "btnUpload": "Загрузить", + "btnUpload": "Выбрать", "btnSyncScroll": "Синхронизировать прокрутку", "btnSettings": "Настройки", "btnExportPdf": "Экспортировать PDF", diff --git a/public/locales/sk/tools.json b/public/locales/sk/tools.json index abc81e4c..b4020a87 100644 --- a/public/locales/sk/tools.json +++ b/public/locales/sk/tools.json @@ -840,7 +840,7 @@ "subtitle": "Napíšte alebo vložte Markdown a exportujte ho ako krásne naformátované PDF.", "paneMarkdown": "Markdown", "panePreview": "Náhľad", - "btnUpload": "Nahrať", + "btnUpload": "Vybrať", "btnSyncScroll": "Synchrónne posúvanie", "btnSettings": "Nastavenia", "btnExportPdf": "Exportovať PDF", diff --git a/public/locales/sv/tools.json b/public/locales/sv/tools.json index d714ac4b..939e3bc1 100644 --- a/public/locales/sv/tools.json +++ b/public/locales/sv/tools.json @@ -840,7 +840,7 @@ "subtitle": "Skriv eller klistra in Markdown och exportera som en vackert formaterad PDF.", "paneMarkdown": "Markdown", "panePreview": "Förhandsvisning", - "btnUpload": "Ladda upp", + "btnUpload": "Välj", "btnSyncScroll": "Synkronisera scroll", "btnSettings": "Inställningar", "btnExportPdf": "Exportera PDF", diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 922fc43e..7bf39a63 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -840,7 +840,7 @@ "subtitle": "Write or paste Markdown and export it as a beautifully formatted PDF.", "paneMarkdown": "Markdown", "panePreview": "Preview", - "btnUpload": "Upload", + "btnUpload": "Seç", "btnSyncScroll": "Sync Scroll", "btnSettings": "Settings", "btnExportPdf": "Export PDF", diff --git a/public/locales/uk/tools.json b/public/locales/uk/tools.json index 8354d3e0..3828b80d 100644 --- a/public/locales/uk/tools.json +++ b/public/locales/uk/tools.json @@ -840,7 +840,7 @@ "subtitle": "Напишіть або вставте Markdown і експортуйте його у красиво оформлений PDF.", "paneMarkdown": "Markdown", "panePreview": "Попередній перегляд", - "btnUpload": "Завантажити", + "btnUpload": "Вибрати", "btnSyncScroll": "Синхронізувати прокрутку", "btnSettings": "Налаштування", "btnExportPdf": "Експортувати у PDF", diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index b5258d72..62b2741d 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -840,7 +840,7 @@ "subtitle": "Viết hoặc dán Markdown và xuất nó thành PDF được định dạng đẹp.", "paneMarkdown": "Markdown", "panePreview": "Xem trước", - "btnUpload": "Tải lên", + "btnUpload": "Chọn", "btnSyncScroll": "Cuộn đồng bộ", "btnSettings": "Cài đặt", "btnExportPdf": "Xuất PDF", diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json index d0a6e195..8b2845a8 100644 --- a/public/locales/zh-TW/tools.json +++ b/public/locales/zh-TW/tools.json @@ -840,7 +840,7 @@ "subtitle": "編寫或貼上 Markdown 並匯出為精美格式的 PDF。", "paneMarkdown": "Markdown", "panePreview": "預覽", - "btnUpload": "上傳", + "btnUpload": "選擇", "btnSyncScroll": "同步捲動", "btnSettings": "設定", "btnExportPdf": "匯出 PDF", diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index dbf7a541..2f6635cc 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -840,7 +840,7 @@ "subtitle": "编写或粘贴 Markdown 并将其导出为精美格式的 PDF。", "paneMarkdown": "Markdown", "panePreview": "预览", - "btnUpload": "上传", + "btnUpload": "选择", "btnSyncScroll": "同步滚动", "btnSettings": "设置", "btnExportPdf": "导出 PDF", diff --git a/src/css/markdown-editor.css b/src/css/markdown-editor.css index 3852036e..1edecdb8 100644 --- a/src/css/markdown-editor.css +++ b/src/css/markdown-editor.css @@ -115,7 +115,7 @@ /* Split View Container */ .md-editor-main { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; height: 600px; border: 1px solid rgb(55, 65, 81); @@ -127,6 +127,7 @@ display: flex; flex-direction: column; min-height: 0; + min-width: 0; background: rgb(31, 41, 55); } @@ -284,6 +285,22 @@ padding-left: 2em; } +.md-editor-preview ul { + list-style-type: disc; +} + +.md-editor-preview ul ul { + list-style-type: circle; +} + +.md-editor-preview ul ul ul { + list-style-type: square; +} + +.md-editor-preview ol { + list-style-type: decimal; +} + .md-editor-preview li { margin: 0.25em 0; } @@ -559,7 +576,6 @@ /* Print Styles */ @media print { - /* Reset all backgrounds, margins, and padding */ html, body { @@ -584,10 +600,10 @@ nav, footer, #back-to-tools, - .bg-gray-800>h1.text-2xl, - .bg-gray-800>p.text-gray-400, - #markdown-editor-container~h1, - #markdown-editor-container~p { + .bg-gray-800 > h1.text-2xl, + .bg-gray-800 > p.text-gray-400, + #markdown-editor-container ~ h1, + #markdown-editor-container ~ p { display: none !important; } @@ -600,7 +616,7 @@ .py-8, .px-4, #markdown-editor-container, - [class*="container"] { + [class*='container'] { padding: 0 !important; margin: 0 !important; background: white !important; @@ -646,6 +662,7 @@ background: white !important; color: black !important; padding: 0 !important; + overflow-wrap: break-word; } .md-editor-preview h1, @@ -658,8 +675,8 @@ page-break-after: avoid; } - .md-editor-preview>h1:first-child, - .md-editor-preview>*:first-child { + .md-editor-preview > h1:first-child, + .md-editor-preview > *:first-child { margin-top: 0 !important; } @@ -682,10 +699,13 @@ background: #f6f8fa !important; border: 1px solid #e1e4e8 !important; page-break-inside: avoid; + white-space: pre-wrap !important; + overflow-wrap: anywhere; } .md-editor-preview pre code { color: #24292e !important; + white-space: pre-wrap !important; } .md-editor-preview blockquote { @@ -697,6 +717,11 @@ page-break-inside: avoid; } + .md-editor-preview table.md-table-break-anywhere th, + .md-editor-preview table.md-table-break-anywhere td { + overflow-wrap: anywhere; + } + .md-editor-preview th, .md-editor-preview td { border-color: #e1e4e8 !important; @@ -750,7 +775,7 @@ } .md-editor-main { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); height: auto; gap: 0; } @@ -814,4 +839,4 @@ .md-editor-actions { gap: 0.375rem; } -} \ No newline at end of file +} diff --git a/src/js/utils/markdown-editor.ts b/src/js/utils/markdown-editor.ts index 5f603097..028f5cf6 100644 --- a/src/js/utils/markdown-editor.ts +++ b/src/js/utils/markdown-editor.ts @@ -75,6 +75,9 @@ export interface MarkdownEditorOptions { export type { MarkdownItOptions } from '@/types'; +const MIN_TABLE_ZOOM = 0.6; +const TABLE_BREAK_CLASS = 'md-table-break-anywhere'; + const DEFAULT_MARKDOWN = `# Welcome to BentoPDF Markdown Editor This is a **live preview** markdown editor with full plugin support. @@ -342,7 +345,7 @@ export class MarkdownEditor {
@@ -439,6 +442,13 @@ export class MarkdownEditor { this.updatePreview(); }); + window + .matchMedia('print') + .addEventListener('change', (event) => + this.fitTablesToPage(event.matches) + ); + window.addEventListener('afterprint', () => this.fitTablesToPage(false)); + // Sync scroll const syncScrollBtn = document.getElementById('mdSyncScroll'); syncScrollBtn?.addEventListener('click', () => { @@ -752,6 +762,26 @@ export class MarkdownEditor { }); } + private fitTablesToPage(printing: boolean): void { + if (!this.preview) return; + const available = this.preview.clientWidth; + for (const table of this.preview.querySelectorAll('table')) { + table.style.zoom = ''; + table.classList.remove(TABLE_BREAK_CLASS); + if (!printing || table.scrollWidth <= available) continue; + if (available / table.scrollWidth < MIN_TABLE_ZOOM) { + table.classList.add(TABLE_BREAK_CLASS); + } + let zoom = 1; + for (let attempt = 0; attempt < 5; attempt++) { + const width = table.getBoundingClientRect().width; + if (width <= available) break; + zoom = Math.floor(((zoom * available) / width) * 100) / 100; + table.style.zoom = String(zoom); + } + } + } + private exportPdf(): void { // Use browser's native print functionality window.print(); From 7eec9a0d644a01fcc01cb896ff6720fd52446868 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Thu, 22 Oct 2026 15:04:42 +0530 Subject: [PATCH 07/10] feat: add favorites functionality to tools --- public/locales/ar/tools.json | 8 + public/locales/be/tools.json | 8 + public/locales/ca/tools.json | 8 + public/locales/da/tools.json | 8 + public/locales/de/tools.json | 8 + public/locales/en/tools.json | 8 + public/locales/es/tools.json | 8 + public/locales/fa/tools.json | 8 + public/locales/fr/tools.json | 8 + public/locales/id/tools.json | 8 + public/locales/it/tools.json | 8 + public/locales/ja/tools.json | 8 + public/locales/ko/tools.json | 8 + public/locales/nl/tools.json | 8 + public/locales/pl/tools.json | 8 + public/locales/pt/tools.json | 8 + public/locales/ru/tools.json | 8 + public/locales/sk/tools.json | 8 + public/locales/sv/tools.json | 8 + public/locales/tr/tools.json | 8 + public/locales/uk/tools.json | 8 + public/locales/vi/tools.json | 8 + public/locales/zh-TW/tools.json | 8 + public/locales/zh/tools.json | 8 + src/css/landing.css | 6 +- src/css/styles.css | 190 ++++++++++++ src/js/main.ts | 450 ++++++++++++++++++++++++++--- src/js/types/index.ts | 1 + src/js/types/tool-category-type.ts | 3 + 29 files changed, 795 insertions(+), 47 deletions(-) create mode 100644 src/js/types/tool-category-type.ts diff --git a/public/locales/ar/tools.json b/public/locales/ar/tools.json index 12f7c8aa..367dd317 100644 --- a/public/locales/ar/tools.json +++ b/public/locales/ar/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "المفضلة", "popularTools": "الأدوات الشائعة", "editAnnotate": "تعديل وتعليق", "convertToPdf": "تحويل إلى PDF", @@ -10,6 +11,13 @@ "input": "إدخال", "output": "إخراج" }, + "favorites": { + "add": "إضافة إلى المفضلة", + "remove": "إزالة من المفضلة", + "hintLabel": "تلميح", + "hint": "اضغط مطولًا على أي أداة لإضافتها إلى المفضلة.", + "hideHint": "إخفاء التلميح" + }, "convertToPdf": "تحويل إلى PDF", "pdfMultiTool": { "name": "أداة PDF المتعددة", diff --git a/public/locales/be/tools.json b/public/locales/be/tools.json index 1fd40b3f..45e8ff53 100644 --- a/public/locales/be/tools.json +++ b/public/locales/be/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Абранае", "popularTools": "Папулярныя інструменты", "editAnnotate": "Рэдагаванне і анатацыі", "convertToPdf": "Канвертацыя ў PDF", @@ -10,6 +11,13 @@ "input": "Увод", "output": "Вывад" }, + "favorites": { + "add": "Дадаць у абранае", + "remove": "Выдаліць з абранага", + "hintLabel": "Парада", + "hint": "Націсніце і ўтрымлівайце інструмент, каб дадаць яго ў абранае.", + "hideHint": "Схаваць параду" + }, "convertToPdf": "Канвертаваць у PDF", "pdfMultiTool": { "name": "Мультыінструмент PDF", diff --git a/public/locales/ca/tools.json b/public/locales/ca/tools.json index 5efa0cd7..cede54fe 100644 --- a/public/locales/ca/tools.json +++ b/public/locales/ca/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Preferits", "popularTools": "Eines populars", "editAnnotate": "Edita i anota", "convertToPdf": "Converteix a PDF", @@ -10,6 +11,13 @@ "input": "Entrada", "output": "Sortida" }, + "favorites": { + "add": "Afegeix als preferits", + "remove": "Treu dels preferits", + "hintLabel": "Consell", + "hint": "Mantén premuda una eina per afegir-la als preferits.", + "hideHint": "Amaga el consell" + }, "convertToPdf": "Converteix a PDF", "pdfMultiTool": { "name": "Eina PDF multifunció", diff --git a/public/locales/da/tools.json b/public/locales/da/tools.json index 4ea3503f..31a9eae6 100644 --- a/public/locales/da/tools.json +++ b/public/locales/da/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoritter", "popularTools": "Populære værktøjer", "editAnnotate": "Rediger og annotér", "convertToPdf": "Konverter til PDF", @@ -10,6 +11,13 @@ "input": "Input", "output": "Output" }, + "favorites": { + "add": "Føj til favoritter", + "remove": "Fjern fra favoritter", + "hintLabel": "Tip", + "hint": "Tryk og hold på et værktøj for at føje det til dine favoritter.", + "hideHint": "Skjul tip" + }, "convertToPdf": "Konvertér til PDF", "pdfMultiTool": { "name": "PDF Multi-værktøj", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 8131602a..001eac54 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoriten", "popularTools": "Beliebte Werkzeuge", "editAnnotate": "Bearbeiten & Annotieren", "convertToPdf": "In PDF konvertieren", @@ -10,6 +11,13 @@ "input": "Eingabe", "output": "Ausgabe" }, + "favorites": { + "add": "Zu Favoriten hinzufügen", + "remove": "Aus Favoriten entfernen", + "hintLabel": "Tipp", + "hint": "Halte ein Werkzeug gedrückt, um es zu deinen Favoriten hinzuzufügen.", + "hideHint": "Tipp ausblenden" + }, "convertToPdf": "In PDF konvertieren", "pdfMultiTool": { "name": "PDF Multi-Werkzeug", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 16128407..609be47d 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favorites", "popularTools": "Popular Tools", "editAnnotate": "Edit & Annotate", "convertToPdf": "Convert to PDF", @@ -10,6 +11,13 @@ "input": "Input", "output": "Output" }, + "favorites": { + "add": "Add to favorites", + "remove": "Remove from favorites", + "hintLabel": "Tip", + "hint": "Press and hold a tool to add it to your favorites.", + "hideHint": "Hide tip" + }, "convertToPdf": "Convert to PDF", "pdfMultiTool": { "name": "PDF Multi Tool", diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json index 680d97f5..d5008004 100644 --- a/public/locales/es/tools.json +++ b/public/locales/es/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoritos", "popularTools": "Herramientas Populares", "editAnnotate": "Editar y Anotar", "convertToPdf": "Convertir a PDF", @@ -10,6 +11,13 @@ "input": "Entrada", "output": "Salida" }, + "favorites": { + "add": "Añadir a favoritos", + "remove": "Quitar de favoritos", + "hintLabel": "Consejo", + "hint": "Mantén pulsada una herramienta para añadirla a tus favoritos.", + "hideHint": "Ocultar consejo" + }, "convertToPdf": "Convertir a PDF", "pdfMultiTool": { "name": "Multiherramienta PDF", diff --git a/public/locales/fa/tools.json b/public/locales/fa/tools.json index 9f866770..560417fa 100644 --- a/public/locales/fa/tools.json +++ b/public/locales/fa/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "علاقه‌مندی‌ها", "popularTools": "ابزارهای پرکاربرد", "editAnnotate": "ویرایش و حاشیه‌نویسی", "convertToPdf": "تبدیل به PDF", @@ -10,6 +11,13 @@ "input": "ورودی", "output": "خروجی" }, + "favorites": { + "add": "افزودن به علاقه‌مندی‌ها", + "remove": "حذف از علاقه‌مندی‌ها", + "hintLabel": "نکته", + "hint": "برای افزودن یک ابزار به علاقه‌مندی‌ها، آن را لمس کرده و نگه دارید.", + "hideHint": "پنهان کردن نکته" + }, "convertToPdf": "تبدیل به PDF", "pdfMultiTool": { "name": "ابزار چندکارهٔ PDF", diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index 93e1dcff..d0059ee0 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoris", "popularTools": "Outils populaires", "editAnnotate": "Éditer et annoter", "convertToPdf": "Convertir en PDF", @@ -10,6 +11,13 @@ "input": "Entrée", "output": "Sortie" }, + "favorites": { + "add": "Ajouter aux favoris", + "remove": "Retirer des favoris", + "hintLabel": "Astuce", + "hint": "Appuyez longuement sur un outil pour l’ajouter à vos favoris.", + "hideHint": "Masquer l’astuce" + }, "convertToPdf": "Convertir en PDF", "pdfMultiTool": { "name": "Outil PDF tout-en-un", diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index d7648eac..0b32483b 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favorit", "popularTools": "Alat Populer", "editAnnotate": "Edit & Anotasi", "convertToPdf": "Konversi ke PDF", @@ -10,6 +11,13 @@ "input": "Masukan", "output": "Keluaran" }, + "favorites": { + "add": "Tambahkan ke favorit", + "remove": "Hapus dari favorit", + "hintLabel": "Tips", + "hint": "Tekan lama sebuah alat untuk menambahkannya ke favorit.", + "hideHint": "Sembunyikan tips" + }, "convertToPdf": "Konversi ke PDF", "pdfMultiTool": { "name": "Alat Multi PDF", diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index e26441f5..8a4a1479 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Preferiti", "popularTools": "Strumenti popolari", "editAnnotate": "Modifica e Annota", "convertToPdf": "Converti in PDF", @@ -10,6 +11,13 @@ "input": "Input", "output": "Output" }, + "favorites": { + "add": "Aggiungi ai preferiti", + "remove": "Rimuovi dai preferiti", + "hintLabel": "Suggerimento", + "hint": "Tieni premuto uno strumento per aggiungerlo ai preferiti.", + "hideHint": "Nascondi suggerimento" + }, "convertToPdf": "Converti in PDF", "pdfMultiTool": { "name": "Strumento PDF multifunzione", diff --git a/public/locales/ja/tools.json b/public/locales/ja/tools.json index 47621da8..2cb20578 100644 --- a/public/locales/ja/tools.json +++ b/public/locales/ja/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "お気に入り", "popularTools": "人気のツール", "editAnnotate": "編集と注釈", "convertToPdf": "PDFに変換", @@ -10,6 +11,13 @@ "input": "入力", "output": "出力" }, + "favorites": { + "add": "お気に入りに追加", + "remove": "お気に入りから削除", + "hintLabel": "ヒント", + "hint": "ツールを長押しするとお気に入りに追加できます。", + "hideHint": "ヒントを非表示" + }, "convertToPdf": "PDFに変換", "pdfMultiTool": { "name": "PDFマルチツール", diff --git a/public/locales/ko/tools.json b/public/locales/ko/tools.json index d3d36196..d7d4d81d 100644 --- a/public/locales/ko/tools.json +++ b/public/locales/ko/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "즐겨찾기", "popularTools": "인기 도구", "editAnnotate": "편집 및 주석", "convertToPdf": "PDF로 변환", @@ -10,6 +11,13 @@ "input": "입력", "output": "출력" }, + "favorites": { + "add": "즐겨찾기에 추가", + "remove": "즐겨찾기에서 제거", + "hintLabel": "팁", + "hint": "도구를 길게 누르면 즐겨찾기에 추가할 수 있습니다.", + "hideHint": "팁 숨기기" + }, "convertToPdf": "PDF로 변환", "pdfMultiTool": { "name": "PDF 멀티 도구", diff --git a/public/locales/nl/tools.json b/public/locales/nl/tools.json index 1059a8f4..44c3e6c9 100644 --- a/public/locales/nl/tools.json +++ b/public/locales/nl/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favorieten", "popularTools": "Populaire Tools", "editAnnotate": "Bewerken & Annoteren", "convertToPdf": "Converteren naar PDF", @@ -10,6 +11,13 @@ "input": "Invoer", "output": "Uitvoer" }, + "favorites": { + "add": "Toevoegen aan favorieten", + "remove": "Verwijderen uit favorieten", + "hintLabel": "Tip", + "hint": "Houd een tool ingedrukt om hem aan je favorieten toe te voegen.", + "hideHint": "Tip verbergen" + }, "convertToPdf": "Naar PDF converteren", "pdfMultiTool": { "name": "PDF Multi-tool", diff --git a/public/locales/pl/tools.json b/public/locales/pl/tools.json index 414b2bd5..d765e941 100644 --- a/public/locales/pl/tools.json +++ b/public/locales/pl/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Ulubione", "popularTools": "Popularne narzędzia", "editAnnotate": "Edytuj i adnotuj", "convertToPdf": "Konwertuj na PDF", @@ -10,6 +11,13 @@ "input": "Wejście", "output": "Wyjście" }, + "favorites": { + "add": "Dodaj do ulubionych", + "remove": "Usuń z ulubionych", + "hintLabel": "Wskazówka", + "hint": "Naciśnij i przytrzymaj narzędzie, aby dodać je do ulubionych.", + "hideHint": "Ukryj wskazówkę" + }, "convertToPdf": "Konwertuj na PDF", "pdfMultiTool": { "name": "Wielofunkcyjne narzędzie PDF", diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json index 0fa23d79..41b287c2 100644 --- a/public/locales/pt/tools.json +++ b/public/locales/pt/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoritos", "popularTools": "Ferramentas Populares", "editAnnotate": "Editar e Anotar", "convertToPdf": "Converter para PDF", @@ -10,6 +11,13 @@ "input": "Entrada", "output": "Saída" }, + "favorites": { + "add": "Adicionar aos favoritos", + "remove": "Remover dos favoritos", + "hintLabel": "Dica", + "hint": "Toque e segure uma ferramenta para adicioná-la aos favoritos.", + "hideHint": "Ocultar dica" + }, "convertToPdf": "Converter para PDF", "pdfMultiTool": { "name": "Multiferramenta PDF", diff --git a/public/locales/ru/tools.json b/public/locales/ru/tools.json index 0f7e931d..60b37754 100644 --- a/public/locales/ru/tools.json +++ b/public/locales/ru/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Избранное", "popularTools": "Популярные инструменты", "editAnnotate": "Редактирование и добавление примечаний", "convertToPdf": "Конвертация в PDF", @@ -10,6 +11,13 @@ "input": "Ввод", "output": "Вывод" }, + "favorites": { + "add": "Добавить в избранное", + "remove": "Удалить из избранного", + "hintLabel": "Совет", + "hint": "Нажмите и удерживайте инструмент, чтобы добавить его в избранное.", + "hideHint": "Скрыть совет" + }, "convertToPdf": "Конвертировать в PDF", "pdfMultiTool": { "name": "PDF мультиинструмент", diff --git a/public/locales/sk/tools.json b/public/locales/sk/tools.json index b4020a87..248a877d 100644 --- a/public/locales/sk/tools.json +++ b/public/locales/sk/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Obľúbené", "popularTools": "Populárne nástroje", "editAnnotate": "Úprava a anotácia", "convertToPdf": "Previesť do PDF", @@ -10,6 +11,13 @@ "input": "Vstup", "output": "Výstup" }, + "favorites": { + "add": "Pridať do obľúbených", + "remove": "Odstrániť z obľúbených", + "hintLabel": "Tip", + "hint": "Podržte prst na nástroji a pridajte ho do obľúbených.", + "hideHint": "Skryť tip" + }, "convertToPdf": "Previesť do PDF", "pdfMultiTool": { "name": "PDF Multi Nástroj", diff --git a/public/locales/sv/tools.json b/public/locales/sv/tools.json index 939e3bc1..805a9bdb 100644 --- a/public/locales/sv/tools.json +++ b/public/locales/sv/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoriter", "popularTools": "Populära verktyg", "editAnnotate": "Redigera & Kommentera", "convertToPdf": "Konvertera till PDF", @@ -10,6 +11,13 @@ "input": "Inmatning", "output": "Utdata" }, + "favorites": { + "add": "Lägg till i favoriter", + "remove": "Ta bort från favoriter", + "hintLabel": "Tips", + "hint": "Tryck och håll på ett verktyg för att lägga till det i dina favoriter.", + "hideHint": "Dölj tips" + }, "convertToPdf": "Konvertera till PDF", "pdfMultiTool": { "name": "PDF multiverktyg", diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 7bf39a63..db74f17e 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Favoriler", "popularTools": "Popüler Araçlar", "editAnnotate": "Düzenle & Açıklama Ekle", "convertToPdf": "PDF'ye Dönüştür", @@ -10,6 +11,13 @@ "input": "Giriş", "output": "Çıkış" }, + "favorites": { + "add": "Favorilere ekle", + "remove": "Favorilerden kaldır", + "hintLabel": "İpucu", + "hint": "Bir aracı favorilerinize eklemek için basılı tutun.", + "hideHint": "İpucunu gizle" + }, "convertToPdf": "PDF'ye Dönüştür", "pdfMultiTool": { "name": "PDF Çoklu Araç", diff --git a/public/locales/uk/tools.json b/public/locales/uk/tools.json index 3828b80d..dc56df5e 100644 --- a/public/locales/uk/tools.json +++ b/public/locales/uk/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Вибране", "popularTools": "Популярні інструменти", "editAnnotate": "Редагування та анотації", "convertToPdf": "Конвертація у PDF", @@ -10,6 +11,13 @@ "input": "Ввід", "output": "Вивід" }, + "favorites": { + "add": "Додати до вибраного", + "remove": "Видалити з вибраного", + "hintLabel": "Порада", + "hint": "Натисніть і утримуйте інструмент, щоб додати його до вибраного.", + "hideHint": "Сховати пораду" + }, "convertToPdf": "Конвертувати в PDF", "pdfMultiTool": { "name": "PDF мультиінструмент", diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index 62b2741d..15269e0e 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "Yêu thích", "popularTools": "Công cụ phổ biến", "editAnnotate": "Chỉnh sửa & Ghi chú", "convertToPdf": "Chuyển đổi sang PDF", @@ -10,6 +11,13 @@ "input": "Đầu vào", "output": "Đầu ra" }, + "favorites": { + "add": "Thêm vào yêu thích", + "remove": "Xóa khỏi yêu thích", + "hintLabel": "Mẹo", + "hint": "Nhấn giữ một công cụ để thêm vào mục yêu thích.", + "hideHint": "Ẩn mẹo" + }, "convertToPdf": "Chuyển đổi sang PDF", "pdfMultiTool": { "name": "Công cụ đa năng PDF", diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json index 8b2845a8..34178de7 100644 --- a/public/locales/zh-TW/tools.json +++ b/public/locales/zh-TW/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "我的最愛", "popularTools": "熱門工具", "editAnnotate": "編輯與註解", "convertToPdf": "轉換為 PDF", @@ -10,6 +11,13 @@ "input": "輸入", "output": "輸出" }, + "favorites": { + "add": "加入我的最愛", + "remove": "從我的最愛移除", + "hintLabel": "提示", + "hint": "長按工具即可加入我的最愛。", + "hideHint": "隱藏提示" + }, "convertToPdf": "轉換為 PDF", "pdfMultiTool": { "name": "PDF 多功能工具", diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index 2f6635cc..92438621 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -1,5 +1,6 @@ { "categories": { + "favorites": "收藏", "popularTools": "热门工具", "editAnnotate": "编辑与注释", "convertToPdf": "转换为 PDF", @@ -10,6 +11,13 @@ "input": "输入", "output": "输出" }, + "favorites": { + "add": "添加到收藏", + "remove": "从收藏中移除", + "hintLabel": "提示", + "hint": "长按工具即可将其添加到收藏。", + "hideHint": "隐藏提示" + }, "convertToPdf": "转换为 PDF", "pdfMultiTool": { "name": "PDF 多功能工具", diff --git a/src/css/landing.css b/src/css/landing.css index fb793816..76891e96 100644 --- a/src/css/landing.css +++ b/src/css/landing.css @@ -1211,7 +1211,8 @@ body.lp ::selection { } .lp .category-group:first-child, -.lp #search-results + .category-group { +.lp #search-results + .category-group, +.lp .tool-fav-hint + .category-group { margin-top: 0; } @@ -1301,7 +1302,8 @@ body.lp ::selection { outline-offset: -2px; } -.lp #tool-grid:not(.compact-mode) .tool-card:hover { +.lp #tool-grid:not(.compact-mode) .tool-card:hover, +.lp #tool-grid:not(.compact-mode) .tool-tile:hover .tool-card { background: var(--lp-bg-3); transform: none; box-shadow: none; diff --git a/src/css/styles.css b/src/css/styles.css index cd1a7796..2168d07c 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -123,6 +123,196 @@ body { transform: rotate(-90deg); } +.tool-tile { + position: relative; + display: flex; + min-width: 0; +} + +.tool-tile > .tool-card { + flex: 1; + min-width: 0; +} + +.tool-fav-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: grid; + place-items: center; + width: 2rem; + height: 2rem; + padding: 0; + border: 0; + border-radius: 0.375rem; + background: none; + color: #9ca3af; + cursor: pointer; + opacity: 0; + transition: + opacity 0.2s, + color 0.2s; +} + +.tool-fav-btn svg { + width: 1.125rem; + height: 1.125rem; + fill: currentColor; +} + +.tool-fav-btn .tool-fav-on, +.tool-fav-btn.is-favorite .tool-fav-off { + display: none; +} + +.tool-fav-btn.is-favorite .tool-fav-on { + display: inline; +} + +.tool-fav-btn:focus-visible, +.tool-fav-btn.is-favorite { + opacity: 1; +} + +@media (hover: hover) { + .tool-tile:hover .tool-fav-btn { + opacity: 1; + } +} + +.tool-fav-btn:hover { + color: #ffffff; +} + +.tool-fav-btn.is-favorite { + color: #e3c46b; +} + +.tool-fav-btn:focus-visible { + outline: 2px solid #a5b4fc; + outline-offset: 0; +} + +.tool-fav-menu, +.tool-fav-hint { + display: none; +} + +@media (hover: none) { + .tool-fav-btn:not(.is-favorite) { + visibility: hidden; + } + + .tool-tile > .tool-card { + -webkit-touch-callout: none; + -webkit-user-drag: none; + -webkit-user-select: none; + user-select: none; + } + + .tool-fav-menu { + position: absolute; + right: 0.75rem; + bottom: 0.75rem; + left: 0.75rem; + z-index: 2; + display: block; + animation: tool-fav-menu-in 0.15s ease-out; + } + + .tool-fav-menu-btn { + display: flex; + align-items: center; + gap: 0.625rem; + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + background: rgb(52, 52, 52); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + color: #f3f3f1; + font-size: 0.9375rem; + font-weight: 500; + text-align: left; + } + + .tool-fav-menu-btn svg { + flex-shrink: 0; + width: 1.125rem; + height: 1.125rem; + fill: #e3c46b; + } + + .tool-fav-menu-btn .tool-fav-on, + .tool-fav-menu-btn.is-favorite .tool-fav-off { + display: none; + } + + .tool-fav-menu-btn.is-favorite .tool-fav-on { + display: inline; + } + + .tool-fav-hint { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.5rem; + padding-block: 0.875rem; + padding-inline: 1.125rem 0.5rem; + border: 1px solid var(--lp-line-2, rgba(255, 255, 255, 0.2)); + background: var(--lp-bg-2, #2a2a2a); + color: var(--lp-text-2, #b4b4b0); + font-size: 0.875rem; + line-height: 1.45; + } + + .tool-fav-hint > span { + flex: 1; + } + + .tool-fav-hint-label { + color: var(--lp-accent-3, #a5b4fc); + font-weight: 600; + } + + .tool-fav-hint-close { + display: grid; + place-items: center; + flex-shrink: 0; + width: 2rem; + height: 2rem; + padding: 0; + border: 0; + background: none; + color: var(--lp-text-2, #b4b4b0); + font-size: 1rem; + } + + #search-results:not(.hidden) ~ .tool-fav-hint { + display: none; + } + + .compact-mode .tool-fav-menu { + top: calc(100% - 0.25rem); + bottom: auto; + } +} + +@keyframes tool-fav-menu-in { + from { + opacity: 0; + transform: translateY(4px); + } +} + +.compact-mode .tool-tile > .tool-card { + padding-right: 2.75rem; +} + +.compact-mode .tool-fav-btn { + top: calc(50% - 1rem); +} + .compact-mode .category-tools { grid-template-columns: 1fr !important; gap: 0 !important; diff --git a/src/js/main.ts b/src/js/main.ts index 51583619..7fe19a61 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -30,6 +30,18 @@ import { removeStoredItem, } from './utils/safe-storage.js'; import { sanitizeTranslationHtml } from './utils/translation-html.js'; +import type { ToolCategory } from '@/types'; + +const FAVORITE_STAR_PATHS = [ + [ + 'off', + 'M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z', + ], + [ + 'on', + 'M234.29,114.85l-45,38.83L203,211.75a16.4,16.4,0,0,1-24.5,17.82L128,198.49,77.47,229.57A16.4,16.4,0,0,1,53,211.75l13.76-58.07-45-38.83A16.46,16.46,0,0,1,31.08,86l59-4.76,22.76-55.08a16.36,16.36,0,0,1,30.27,0l22.75,55.08,59,4.76a16.46,16.46,0,0,1,9.37,28.86Z', + ], +] as const; declare const __BRAND_NAME__: string; const init = async () => { @@ -159,6 +171,7 @@ const init = async () => { } const categoryTranslationKeys: Record = { + Favorites: 'tools:categories.favorites', 'Popular Tools': 'tools:categories.popularTools', 'Edit & Annotate': 'tools:categories.editAnnotate', 'Convert to PDF': 'tools:categories.convertToPdf', @@ -313,7 +326,277 @@ const init = async () => { })) .filter((category) => category.tools.length > 0); - filteredCategories.forEach((category) => { + const availableTools = new Map(); + filteredCategories.forEach((category) => + category.tools.forEach((tool) => { + if (!availableTools.has(tool.id)) availableTools.set(tool.id, tool); + }) + ); + + let favoriteToolIds: string[] = []; + let unavailableFavoriteIds: string[] = []; + + function loadFavorites() { + let stored: unknown = []; + try { + stored = JSON.parse(getStoredItem('favoriteTools') || '[]'); + } catch { + removeStoredItem('favoriteTools'); + } + const ids = Array.isArray(stored) + ? [ + ...new Set( + stored.filter((id): id is string => typeof id === 'string') + ), + ] + : []; + favoriteToolIds = ids.filter((id) => availableTools.has(id)); + unavailableFavoriteIds = ids.filter((id) => !availableTools.has(id)); + } + + loadFavorites(); + + function createFavoriteButton(toolId: string): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'tool-fav-btn'; + button.dataset.favId = toolId; + button.appendChild(createStarIcon()); + updateFavoriteButton(button); + return button; + } + + function createStarIcon(): SVGSVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 256 256'); + svg.setAttribute('aria-hidden', 'true'); + for (const [kind, d] of FAVORITE_STAR_PATHS) { + const path = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ); + path.setAttribute('class', `tool-fav-${kind}`); + path.setAttribute('d', d); + svg.appendChild(path); + } + return svg; + } + + const favoritesHint = document.createElement('div'); + favoritesHint.className = 'tool-fav-hint col-span-full'; + const favoritesHintText = document.createElement('span'); + const favoritesHintLabel = document.createElement('strong'); + favoritesHintLabel.className = 'tool-fav-hint-label'; + favoritesHintLabel.textContent = t('tools:favorites.hintLabel'); + favoritesHintText.append( + favoritesHintLabel, + document.createTextNode(` ${t('tools:favorites.hint')}`) + ); + const favoritesHintClose = document.createElement('button'); + favoritesHintClose.type = 'button'; + favoritesHintClose.className = 'tool-fav-hint-close'; + favoritesHintClose.setAttribute( + 'aria-label', + t('tools:favorites.hideHint') + ); + const favoritesHintIcon = document.createElement('i'); + favoritesHintIcon.className = 'ph ph-x'; + favoritesHintIcon.setAttribute('aria-hidden', 'true'); + favoritesHintClose.appendChild(favoritesHintIcon); + favoritesHint.append(favoritesHintText, favoritesHintClose); + favoritesHintClose.addEventListener('click', () => { + setStoredItem('favoritesHintHidden', 'true'); + favoritesHint.remove(); + }); + + function updateFavoritesHint() { + if ( + favoriteToolIds.length > 0 || + getStoredItem('favoritesHintHidden') === 'true' + ) { + favoritesHint.remove(); + return; + } + if (favoritesHint.isConnected) return; + const results = document.getElementById('search-results'); + dom.toolGrid.insertBefore( + favoritesHint, + results ? results.nextSibling : dom.toolGrid.firstChild + ); + } + + const favoriteMenu = document.createElement('div'); + favoriteMenu.className = 'tool-fav-menu'; + const favoriteMenuButton = document.createElement('button'); + favoriteMenuButton.type = 'button'; + favoriteMenuButton.className = 'tool-fav-menu-btn'; + const favoriteMenuLabel = document.createElement('span'); + favoriteMenuButton.append(createStarIcon(), favoriteMenuLabel); + favoriteMenu.appendChild(favoriteMenuButton); + let favoriteMenuStar: HTMLElement | null = null; + + function openFavoriteMenu(tile: HTMLElement) { + const star = tile.querySelector('.tool-fav-btn'); + if (!star) return; + favoriteMenuStar = star; + const active = favoriteToolIds.includes(star.dataset.favId || ''); + favoriteMenuButton.classList.toggle('is-favorite', active); + favoriteMenuLabel.textContent = active + ? t('tools:favorites.remove') + : t('tools:favorites.add'); + tile.appendChild(favoriteMenu); + if (navigator.userActivation?.hasBeenActive) navigator.vibrate?.(10); + } + + function closeFavoriteMenu() { + favoriteMenu.remove(); + favoriteMenuStar = null; + } + + favoriteMenuButton.addEventListener('click', (event) => { + event.stopPropagation(); + const star = favoriteMenuStar; + closeFavoriteMenu(); + if (star) toggleFavorite(star); + }); + + function updateFavoriteButton(button: HTMLElement) { + const active = favoriteToolIds.includes(button.dataset.favId || ''); + button.classList.toggle('is-favorite', active); + button.setAttribute('aria-pressed', String(active)); + const label = active + ? t('tools:favorites.remove') + : t('tools:favorites.add'); + button.setAttribute('aria-label', label); + button.title = label; + } + + function renderFavorites() { + const tools = favoriteToolIds.flatMap((id) => { + const tool = availableTools.get(id); + return tool ? [tool] : []; + }); + if (tools.length === 0) return; + const group = createCategoryGroup({ name: 'Favorites', tools }); + const searchBox = document.getElementById( + 'search-bar' + ) as HTMLInputElement | null; + if (searchBox?.value.trim()) group.style.display = 'none'; + const results = document.getElementById('search-results'); + dom.toolGrid.insertBefore( + group, + results ? results.nextSibling : dom.toolGrid.firstChild + ); + createIcons({ icons }); + } + + function toggleFavorite(button: HTMLElement) { + const toolId = button.dataset.favId; + const tool = toolId ? availableTools.get(toolId) : undefined; + if (!toolId || !tool) return; + const adding = !favoriteToolIds.includes(toolId); + favoriteToolIds = adding + ? [...favoriteToolIds, toolId] + : favoriteToolIds.filter((id) => id !== toolId); + setStoredItem( + 'favoriteTools', + JSON.stringify([...favoriteToolIds, ...unavailableFavoriteIds]) + ); + + const anchorTop = button.getBoundingClientRect().top; + const keyboardFocus = button.matches(':focus-visible'); + const group = dom.toolGrid.querySelector( + ':scope > .is-cat-favorites' + ); + let nextFocus: HTMLElement | null = null; + + if (adding && group) { + group + .querySelector('.category-tools') + ?.appendChild(createToolTile(tool)); + createIcons({ icons }); + } else if (adding) { + renderFavorites(); + } else if (group) { + const tile = group + .querySelector(`.tool-fav-btn[data-fav-id="${CSS.escape(toolId)}"]`) + ?.closest('.tool-tile'); + if (tile?.contains(button)) { + const neighbor = + tile.nextElementSibling ?? tile.previousElementSibling; + nextFocus = + neighbor?.querySelector('.tool-fav-btn') ?? + group.nextElementSibling?.querySelector( + '.category-header' + ) ?? + null; + } + tile?.remove(); + if (favoriteToolIds.length === 0) group.remove(); + } + + dom.toolGrid + .querySelectorAll('.tool-fav-btn') + .forEach((other) => { + if (other.dataset.favId === toolId) updateFavoriteButton(other); + }); + updateFavoritesHint(); + + if (button.isConnected) { + const shift = button.getBoundingClientRect().top - anchorTop; + if (shift) window.scrollBy({ top: shift, behavior: 'instant' }); + } else if (keyboardFocus) { + nextFocus?.focus({ preventScroll: true }); + } + } + + function createToolTile(tool: ToolCategory['tools'][number]) { + const tile = document.createElement('div'); + tile.className = 'tool-tile'; + let toolCard: HTMLDivElement | HTMLAnchorElement; + + if (tool.href) { + toolCard = document.createElement('a'); + toolCard.href = tool.href; + toolCard.className = + 'tool-card block bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center no-underline hover:shadow-lg transition duration-200'; + } else { + toolCard = document.createElement('div'); + toolCard.className = + 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center hover:shadow-lg transition duration-200'; + toolCard.dataset.toolId = getToolId(tool); + } + + const icon = document.createElement('i'); + icon.className = 'w-10 h-10 mb-3 text-indigo-400'; + + if (tool.icon.startsWith('ph-')) { + icon.className = `ph ${tool.icon} text-4xl mb-3 text-indigo-400`; + } else { + icon.setAttribute('data-lucide', tool.icon); + } + + const toolName = document.createElement('h3'); + toolName.className = 'font-semibold text-white'; + const toolKey = toolTranslationKeys[tool.name]; + toolName.textContent = toolKey ? t(`${toolKey}.name`) : tool.name; + + toolCard.append(icon, toolName); + + if (tool.subtitle) { + const toolSubtitle = document.createElement('p'); + toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2'; + toolSubtitle.textContent = toolKey + ? t(`${toolKey}.subtitle`) + : tool.subtitle; + toolCard.appendChild(toolSubtitle); + } + + tile.append(toolCard, createFavoriteButton(tool.id)); + return tile; + } + + function createCategoryGroup(category: ToolCategory): HTMLDivElement { const categoryGroup = document.createElement('div'); const categorySlug = category.name .toLowerCase() @@ -376,69 +659,40 @@ const init = async () => { }); category.tools.forEach((tool) => { - let toolCard: HTMLDivElement | HTMLAnchorElement; - - if (tool.href) { - toolCard = document.createElement('a'); - toolCard.href = tool.href; - toolCard.className = - 'tool-card block bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center no-underline hover:shadow-lg transition duration-200'; - } else { - toolCard = document.createElement('div'); - toolCard.className = - 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center hover:shadow-lg transition duration-200'; - toolCard.dataset.toolId = getToolId(tool); - } - - const icon = document.createElement('i'); - icon.className = 'w-10 h-10 mb-3 text-indigo-400'; - - if (tool.icon.startsWith('ph-')) { - icon.className = `ph ${tool.icon} text-4xl mb-3 text-indigo-400`; - } else { - icon.setAttribute('data-lucide', tool.icon); - } - - const toolName = document.createElement('h3'); - toolName.className = 'font-semibold text-white'; - const toolKey = toolTranslationKeys[tool.name]; - toolName.textContent = toolKey ? t(`${toolKey}.name`) : tool.name; - - toolCard.append(icon, toolName); - - if (tool.subtitle) { - const toolSubtitle = document.createElement('p'); - toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2'; - toolSubtitle.textContent = toolKey - ? t(`${toolKey}.subtitle`) - : tool.subtitle; - toolCard.appendChild(toolSubtitle); - } - - toolsContainer.appendChild(toolCard); + toolsContainer.appendChild(createToolTile(tool)); }); categoryGroup.append(header, toolsContainer); - dom.toolGrid.appendChild(categoryGroup); if (!isCollapsed) { toolsContainer.style.maxHeight = 'none'; toolsContainer.style.overflow = 'visible'; } + + return categoryGroup; + } + + filteredCategories.forEach((category) => { + dom.toolGrid.appendChild(createCategoryGroup(category)); }); const searchBar = document.getElementById('search-bar'); - const categoryGroups = dom.toolGrid.querySelectorAll('.category-group'); const searchResultsContainer = document.createElement('div'); searchResultsContainer.id = 'search-results'; searchResultsContainer.className = 'hidden grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6 col-span-full'; dom.toolGrid.insertBefore(searchResultsContainer, dom.toolGrid.firstChild); + renderFavorites(); + updateFavoritesHint(); searchBar.addEventListener('input', () => { // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message const searchTerm = searchBar.value.toLowerCase().trim(); + closeFavoriteMenu(); + const categoryGroups = dom.toolGrid.querySelectorAll( + ':scope > .category-group' + ); if (!searchTerm) { searchResultsContainer.classList.add('hidden'); @@ -487,7 +741,11 @@ const init = async () => { if (isMatch && !isDuplicate) { seenToolIds.add(toolId); - allTools.push(card.cloneNode(true) as HTMLElement); + allTools.push( + (card.closest('.tool-tile') ?? card).cloneNode( + true + ) as HTMLElement + ); } }); }); @@ -511,8 +769,110 @@ const init = async () => { } }); - dom.toolGrid.addEventListener('click', () => { + dom.toolGrid.addEventListener('click', (event) => { // All tools now use href and navigate directly - no modal handling needed + const favoriteButton = (event.target as Element).closest( + '.tool-fav-btn' + ); + if (favoriteButton) toggleFavorite(favoriteButton); + }); + + let pressTimer = 0; + let pressStart: { x: number; y: number } | null = null; + let suppressClick = false; + const touchOnly = window.matchMedia('(hover: none)'); + + function cancelPress() { + window.clearTimeout(pressTimer); + pressStart = null; + } + + function pressedTile(target: EventTarget | null): HTMLElement | null { + const element = target instanceof Element ? target : null; + if (!element || element.closest('.tool-fav-btn, .tool-fav-menu')) { + return null; + } + return element.closest('.tool-tile'); + } + + dom.toolGrid.addEventListener('pointerdown', (event) => { + suppressClick = false; + if (event.pointerType !== 'touch' || !touchOnly.matches) return; + const tile = pressedTile(event.target); + if (!tile) return; + pressStart = { x: event.clientX, y: event.clientY }; + pressTimer = window.setTimeout(() => { + pressStart = null; + suppressClick = true; + openFavoriteMenu(tile); + }, 500); + }); + + dom.toolGrid.addEventListener('pointermove', (event) => { + if ( + pressStart && + Math.hypot(event.clientX - pressStart.x, event.clientY - pressStart.y) > + 10 + ) { + cancelPress(); + } + }); + + dom.toolGrid.addEventListener('pointerup', cancelPress); + dom.toolGrid.addEventListener('pointercancel', cancelPress); + + dom.toolGrid.addEventListener('contextmenu', (event) => { + if (!touchOnly.matches) return; + const tile = pressedTile(event.target); + if (!tile) return; + event.preventDefault(); + cancelPress(); + if (!tile.contains(favoriteMenu)) { + suppressClick = true; + openFavoriteMenu(tile); + } + }); + + dom.toolGrid.addEventListener( + 'click', + (event) => { + if (!suppressClick) return; + suppressClick = false; + event.preventDefault(); + event.stopPropagation(); + }, + true + ); + + document.addEventListener('pointerdown', (event) => { + if ( + favoriteMenu.isConnected && + !(event.target instanceof Node && favoriteMenu.contains(event.target)) + ) { + closeFavoriteMenu(); + } + }); + window.addEventListener('scroll', closeFavoriteMenu, { passive: true }); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') closeFavoriteMenu(); + }); + + window.addEventListener('storage', (event) => { + if ( + event.key !== null && + event.key !== 'favoriteTools' && + event.key !== 'favoritesHintHidden' + ) { + return; + } + closeFavoriteMenu(); + loadFavorites(); + dom.toolGrid.querySelector(':scope > .is-cat-favorites')?.remove(); + renderFavorites(); + dom.toolGrid + .querySelectorAll('.tool-fav-btn') + .forEach(updateFavoriteButton); + updateFavoritesHint(); }); } diff --git a/src/js/types/index.ts b/src/js/types/index.ts index 1a9624af..cf843835 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -71,3 +71,4 @@ export * from './edit-pdf-redaction-type.ts'; export * from './validate-signature-extraction-type.ts'; export * from './pdf-content-tokens-type.ts'; export * from './compress-pdf-type.ts'; +export * from './tool-category-type.ts'; diff --git a/src/js/types/tool-category-type.ts b/src/js/types/tool-category-type.ts new file mode 100644 index 00000000..5e925404 --- /dev/null +++ b/src/js/types/tool-category-type.ts @@ -0,0 +1,3 @@ +import type { categories } from '../config/tools.ts'; + +export type ToolCategory = (typeof categories)[number]; From 6e8e012d3d220f7492c429cde3755737b4598827 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 23 Oct 2026 15:01:31 +0530 Subject: [PATCH 08/10] fix: serve canonical, hreflang and sitemap URLs and restore Export in the PDF editor --- README.md | 14 ++--- about.html | 5 +- docs/.vitepress/config.mts | 4 +- docs/getting-started.md | 2 +- docs/licensing.md | 2 +- docs/self-hosting/index.md | 2 +- index.html | 6 +-- nginx.conf | 19 ++++--- scripts/enhance-seo-pages.mjs | 4 +- scripts/generate-blog.mjs | 8 +-- scripts/generate-i18n-pages.mjs | 10 ++-- scripts/generate-sitemap.mjs | 4 +- scripts/include-docs-in-dist.js | 4 +- scripts/seo-audit.mjs | 28 ++++++++-- src/js/i18n/i18n.ts | 9 +++- src/js/logic/edit-pdf-page.ts | 22 +++++--- src/js/types/edit-pdf-type.ts | 13 +++++ src/partials/navbar.html | 4 +- vercel.json | 91 +++++++++++++++++++++++++++++++++ vite.config.ts | 11 +++- 20 files changed, 209 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 60796a56..ea008571 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ Have questions, feature requests, or want to chat with the community? Join our D ## 📚 Documentation -[![Documentation](https://img.shields.io/badge/Docs-VitePress-646cff?style=for-the-badge&logo=vite&logoColor=white)](https://bentopdf.com/docs/) +[![Documentation](https://img.shields.io/badge/Docs-VitePress-646cff?style=for-the-badge&logo=vite&logoColor=white)](https://www.bentopdf.com/docs) -Visit our [Documentation](https://bentopdf.com/docs/) for: +Visit our [Documentation](https://www.bentopdf.com/docs) for: - **Getting Started** guide - **Tools Reference** (50+ tools) @@ -100,7 +100,7 @@ BentoPDF is **dual-licensed** to fit your needs: > **One-time purchase** · **Unlimited devices & users** · **Lifetime updates** · **No AGPL obligations** -📖 For more details, see our [Licensing Page](https://bentopdf.com/licensing.html) +📖 For more details, see our [Licensing Page](https://www.bentopdf.com/licensing) ### AGPL Components (Pre-configured via CDN) @@ -810,7 +810,7 @@ systemctl --user daemon-reload systemctl --user enable --now bentopdf ``` -For detailed Quadlet configuration, see [Self-Hosting Docker Guide](https://bentopdf.com/docs/self-hosting/docker). +For detailed Quadlet configuration, see [Self-Hosting Docker Guide](https://www.bentopdf.com/docs/self-hosting/docker). ### 🏢 Self-Hosted build (Simple Mode) @@ -866,7 +866,7 @@ Or set the same variables when building from source — see [Custom Branding](#- > - **AGPL-3.0** (free): allowed if your deployment publishes its full source code under AGPL — this includes any branding modifications, custom configuration, and any code you build on top of it. > - **Commercial license** ($79 lifetime): required for closed-source / proprietary deployments — e.g., a private SaaS where you don't open-source your branding fork or surrounding business logic. > -> See the [Licensing page](https://bentopdf.com/licensing.html) for the full comparison. +> See the [Licensing page](https://www.bentopdf.com/licensing) for the full comparison. ### 🎨 Custom Branding @@ -930,7 +930,7 @@ docker run -d -p 3000:8080 \ ghcr.io/alam00000/bentopdf-simple:latest ``` -Both methods can be combined — the lists are merged. For the full list of tool IDs, see the [self-hosting docs](https://bentopdf.com/docs/self-hosting/docker#disabling-specific-tools). +Both methods can be combined — the lists are merged. For the full list of tool IDs, see the [self-hosting docs](https://www.bentopdf.com/docs/self-hosting/docker#disabling-specific-tools). You can also disable specific features inside the PDF Editor (e.g., redaction, forms) without disabling the entire editor. Add `editorDisabledCategories` to your `config.json`: @@ -940,7 +940,7 @@ You can also disable specific features inside the PDF Editor (e.g., redaction, f } ``` -For the full list of editor categories, see the [self-hosting docs](https://bentopdf.com/docs/self-hosting/docker#disabling-editor-features). +For the full list of editor categories, see the [self-hosting docs](https://www.bentopdf.com/docs/self-hosting/docker#disabling-editor-features). ### 🔕 Disabling the GitHub Star Counter diff --git a/about.html b/about.html index be49594a..d2513b8e 100644 --- a/about.html +++ b/about.html @@ -332,7 +332,10 @@ - + Docs diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index b1ff8b2d..bad15862 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -11,8 +11,8 @@ export default defineConfig({ transformPageData(pageData) { const relPath = pageData.relativePath.replace(/\.md$/, '') - const slug = relPath === 'index' ? '' : relPath.replace(/\/index$/, '/') - const canonicalUrl = slug ? `${SITE_URL}/docs/${slug}` : `${SITE_URL}/docs/` + const slug = relPath === 'index' ? '' : relPath.replace(/\/index$/, '') + const canonicalUrl = slug ? `${SITE_URL}/docs/${slug}` : `${SITE_URL}/docs` pageData.frontmatter.head ??= [] pageData.frontmatter.head.push(['link', { rel: 'canonical', href: canonicalUrl }]) }, diff --git a/docs/getting-started.md b/docs/getting-started.md index a6107893..064d620c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,7 +10,7 @@ BentoPDF is a free, open-source, privacy-first PDF toolkit that runs **entirely ### Option 1: Use the Hosted Version -Visit [bentopdf.com](https://bentopdf.com) to use BentoPDF instantly—no installation required. +Visit [bentopdf.com](https://www.bentopdf.com) to use BentoPDF instantly—no installation required. ### Option 2: Self-Host with Docker diff --git a/docs/licensing.md b/docs/licensing.md index 791e84d5..e9e24f0b 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -3,7 +3,7 @@ BentoPDF is dual-licensed under **AGPL-3.0** (for open-source projects) and a **Commercial License** (for proprietary/closed-source applications). ::: tip Full Details -For complete licensing information, delivery details, AGPL component notices, and invoicing, visit our [Licensing Page](https://bentopdf.com/licensing.html). +For complete licensing information, delivery details, AGPL component notices, and invoicing, visit our [Licensing Page](https://www.bentopdf.com/licensing). ::: ## Get a Commercial License diff --git a/docs/self-hosting/index.md b/docs/self-hosting/index.md index 35be1b6b..365322cc 100644 --- a/docs/self-hosting/index.md +++ b/docs/self-hosting/index.md @@ -148,7 +148,7 @@ Running the Commercial build is allowed under either of BentoPDF's two license o - **AGPL-3.0** (free) — allowed if your deployment publishes its full source code under AGPL, including any branding modifications and surrounding business logic. - **Commercial license** ($79 lifetime) — required for closed-source / proprietary deployments where you don't open-source your branding fork or business code. -See the [Licensing page](https://bentopdf.com/licensing.html) for the full comparison. AGPL-licensed WASM modules (PyMuPDF, Ghostscript, CoherentPDF) load from a CDN at runtime, so they don't enter your image and don't change your licensing posture. +See the [Licensing page](https://www.bentopdf.com/licensing) for the full comparison. AGPL-licensed WASM modules (PyMuPDF, Ghostscript, CoherentPDF) load from a CDN at runtime, so they don't enter your image and don't change your licensing posture. ::: ### Base URL diff --git a/index.html b/index.html index 0f33e5a4..c6698d8d 100644 --- a/index.html +++ b/index.html @@ -559,7 +559,7 @@ class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" > GDPR compliance @@ -584,7 +584,7 @@ class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" > CCPA compliance @@ -609,7 +609,7 @@ class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" > HIPAA compliance diff --git a/nginx.conf b/nginx.conf index 6ddc157f..2fc61166 100644 --- a/nginx.conf +++ b/nginx.conf @@ -110,11 +110,8 @@ http { return 301 $1/$2$3; } - if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca))/([a-z0-9][a-z0-9-]*)/(\?.*)?$) { - return 301 $1/$2$3; - } - if ($request_uri ~ ^/((?!(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca|docs|blog)/)[a-z0-9][a-z0-9-]*)/(\?.*)?$) { - return 301 /$1$2; + if ($request_uri ~ ^(/[^?]*[^/?])/+(\?.*)?$) { + return 301 $1$2; } location ~ /src/pages/ { @@ -122,7 +119,7 @@ http { } location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pl|pt|ru|sk|sv|tr|vi|zh|zh-TW|uk|ja|ca)(/.*)?$ { - try_files $uri $uri/ $uri.html =404; + try_files $uri $uri.html $uri/index.html =404; expires 5m; } @@ -174,14 +171,20 @@ http { error_page 404 /404.html; + location = /docs { + include /etc/nginx/security-headers-docs.conf; + try_files /docs/index.html =404; + expires 5m; + } + location ^~ /docs/ { include /etc/nginx/security-headers-docs.conf; - try_files $uri $uri/ $uri.html /docs/index.html =404; + try_files $uri $uri.html $uri/index.html /docs/index.html =404; expires 5m; } location / { - try_files $uri $uri/ $uri.html =404; + try_files $uri $uri.html $uri/index.html =404; expires 5m; } } diff --git a/scripts/enhance-seo-pages.mjs b/scripts/enhance-seo-pages.mjs index 2d3ca4f1..155f77db 100644 --- a/scripts/enhance-seo-pages.mjs +++ b/scripts/enhance-seo-pages.mjs @@ -8,6 +8,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DIST_DIR = path.resolve(__dirname, '../dist'); +const BASE_PATH = (process.env.BASE_URL || '/').replace(/\/$/, ''); const SITE_URL = (process.env.SITE_URL || 'https://www.bentopdf.com').replace( /\/+$/, '' @@ -125,7 +126,8 @@ function rewriteInternalLinks(document, knownSlugs) { ); if (!match) continue; if (!knownSlugs.has(match[1])) continue; - const target = match[1] === 'index' ? '/' : `/${match[1]}`; + const target = + match[1] === 'index' ? BASE_PATH || '/' : `${BASE_PATH}/${match[1]}`; link.setAttribute('href', `${target}${match[2] || ''}`); } } diff --git a/scripts/generate-blog.mjs b/scripts/generate-blog.mjs index 2474787e..b4167eb7 100644 --- a/scripts/generate-blog.mjs +++ b/scripts/generate-blog.mjs @@ -274,7 +274,7 @@ ${renderFaqs(meta.faqs)} '@type': 'ListItem', position: 1, name: 'Blog', - item: `${SITE_URL}/blog/`, + item: `${SITE_URL}/blog`, }, { '@type': 'ListItem', @@ -319,9 +319,9 @@ function renderIndex(posts) { content="Guides and honest comparisons from the maintainer of BentoPDF: how PDF tools handle your files, and how to get things done without uploading them." /> - + - + file.replace('.html', '')); for (const name of blogFiles) { const url = - name === 'index' ? `${SITE_URL}/blog/` : `${SITE_URL}/blog/${name}`; + name === 'index' ? `${SITE_URL}/blog` : `${SITE_URL}/blog/${name}`; const sourcePath = path.join(projectRoot, 'blog', `${name}.html`); let lastmod; try { diff --git a/scripts/include-docs-in-dist.js b/scripts/include-docs-in-dist.js index c1e97563..b76b270c 100644 --- a/scripts/include-docs-in-dist.js +++ b/scripts/include-docs-in-dist.js @@ -111,7 +111,7 @@ function listDocsHtml(dir, prefix) { ); } else if (entry.name.endsWith('.html') && entry.name !== '404.html') { const slug = entry.name.replace(/\.html$/, ''); - urls.push(slug === 'index' ? `${prefix}/` : `${prefix}/${slug}`); + urls.push(slug === 'index' ? prefix : `${prefix}/${slug}`); } } return urls; @@ -130,7 +130,7 @@ function addDocsToSitemap() { return; } let xml = fs.readFileSync(sitemapPath, 'utf-8'); - if (xml.includes(`${siteUrl}/docs/`)) return; + if (xml.includes(`${siteUrl}/docs`)) return; const urls = listDocsHtml(targetDocsPath, '/docs'); const entries = urls diff --git a/scripts/seo-audit.mjs b/scripts/seo-audit.mjs index ce48a327..68ab7212 100644 --- a/scripts/seo-audit.mjs +++ b/scripts/seo-audit.mjs @@ -85,6 +85,11 @@ for (const lang of LOCALE_DIRS) { : {}; } +function hasTrailingSlash(href) { + const { pathname } = new URL(href); + return pathname !== '/' && pathname.endsWith('/'); +} + function expectedCanonicalForFile(rel) { const parts = rel.split('/'); const fileName = parts.pop(); @@ -121,7 +126,11 @@ function auditHtml(file, knownSlugs) { if (!isLocalized) { const internal = findAll(html, /]+href=["'](\/[^"'#?]*)["']/g); for (const href of internal) { - const clean = href.replace(/\/$/, ''); + const clean = ( + BASE_PATH && (href === BASE_PATH || href.startsWith(`${BASE_PATH}/`)) + ? href.slice(BASE_PATH.length) + : href + ).replace(/\/$/, ''); if (clean === '') continue; const seg = clean.slice(1); if (seg.includes('/')) continue; @@ -162,6 +171,12 @@ function auditHtml(file, knownSlugs) { } try { const url = new URL(actual); + if (hasTrailingSlash(actual)) { + fail( + 'canonical', + `${file.rel}: canonical "${actual}" ends with "/" (the server redirects it to the slashless form)` + ); + } if (url.hostname !== HOST) { warn( 'canonical', @@ -230,11 +245,10 @@ function auditHtml(file, knownSlugs) { `${file.rel}: hreflang href host "${url.hostname}" != expected "${HOST}"` ); } - const trimmed = url.pathname.replace(/^\/|\/$/g, ''); - if (LOCALE_DIRS.has(trimmed) && !url.pathname.endsWith('/')) { + if (hasTrailingSlash(m[1])) { fail( 'hreflang', - `${file.rel}: language-home hreflang "${m[1]}" must end with "/" (the server 301s the slashless form)` + `${file.rel}: hreflang "${m[1]}" ends with "/" (the server redirects it to the slashless form)` ); } } catch { @@ -334,6 +348,12 @@ function auditSitemap() { try { const url = new URL(loc); + if (hasTrailingSlash(loc)) { + fail( + 'sitemap', + `sitemap URL ends with "/" (the server redirects it to the slashless form): ${loc}` + ); + } if (url.hostname !== HOST) { fail( 'sitemap', diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index f5f1aa9a..2b8d21db 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -184,7 +184,10 @@ export const changeLanguage = (lang: SupportedLanguage): void => { if (lang === 'en') { newRelativePath = pagePathWithoutLang; } else { - newRelativePath = `/${lang}${pagePathWithoutLang}`; + newRelativePath = + pagePathWithoutLang === '/' + ? `/${lang}` + : `/${lang}${pagePathWithoutLang}`; } let newPath: string; @@ -293,7 +296,9 @@ export const rewriteLinks = (): void => { newHref = `/${currentLang}/${href}`; } - newHref = newHref.replace(/([^:])\/+/g, '$1/'); + newHref = newHref + .replace(/([^:])\/+/g, '$1/') + .replace(/^([^?#]*[^/?#])\/(?=$|[?#])/, '$1'); link.setAttribute('href', newHref); }); diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index 5d4ba933..23438da6 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -20,6 +20,7 @@ import type { AppliedRedactionTracker, DocManagerPlugin, FreeTextSystemFontAnnotation, + ViewerCommandsPlugin, RedactionPluginLite, } from '@/types'; @@ -328,12 +329,8 @@ async function handleFiles(files: FileList) { pdfWrapper.classList.remove('hidden'); const { default: EmbedPDF } = await import('bentopdf-viewer'); - const disabledCategories = [ - ...getEditorDisabledCategories(), - 'document-export', - ]; viewerInstance = EmbedPDF.init({ - disabledCategories, + disabledCategories: getEditorDisabledCategories(), type: 'container', target: pdfContainer, worker: true, @@ -425,7 +422,7 @@ async function handleFiles(files: FileList) { } downloadBtn.classList.remove('hidden'); - downloadBtn.onclick = async () => { + const downloadEditedPdf = async () => { try { const documentId = activeDocuments.getActiveDocumentId(); const pendingCount = redactionPlugin?.getState().pendingCount ?? 0; @@ -541,6 +538,19 @@ async function handleFiles(files: FileList) { showAlert('Error', 'Failed to download the edited PDF.'); } }; + downloadBtn.onclick = downloadEditedPdf; + + const commands = registry.getPlugin('commands')?.provides() as unknown as + | ViewerCommandsPlugin + | undefined; + commands?.unregisterCommand('document:export'); + commands?.registerCommand({ + id: 'document:export', + labelKey: 'document.export', + icon: 'download', + categories: ['document', 'document-export'], + action: () => void downloadEditedPdf(), + }); const backBtn = document.getElementById('back-to-tools'); if (backBtn) { diff --git a/src/js/types/edit-pdf-type.ts b/src/js/types/edit-pdf-type.ts index 90fa11b2..4bc655a8 100644 --- a/src/js/types/edit-pdf-type.ts +++ b/src/js/types/edit-pdf-type.ts @@ -14,6 +14,19 @@ export interface DocManagerPlugin { saveAsCopy: (id: string) => Promise; } +export interface ViewerCommand { + id: string; + labelKey: string; + icon: string; + categories: string[]; + action: () => void; +} + +export interface ViewerCommandsPlugin { + registerCommand: (command: ViewerCommand) => void; + unregisterCommand: (id: string) => void; +} + export interface EditorAnnotationRect { origin: { x: number; y: number }; size: { width: number; height: number }; diff --git a/src/partials/navbar.html b/src/partials/navbar.html index 9604af02..64db4659 100644 --- a/src/partials/navbar.html +++ b/src/partials/navbar.html @@ -55,7 +55,7 @@
DocsHyper Compress Docs fs.existsSync(candidate)); + if (!diskPath) continue; const source = fs.readFileSync(diskPath, 'utf8'); const updated = source .replace(hrefRegex, `href="${normalizedBase}`) + .replaceAll( + `href="${normalizedBase}"`, + `href="${normalizedBase.slice(0, -1)}"` + ) .replace(srcRegex, `src="${normalizedBase}`) .replace(contentRegex, `content="${normalizedBase}`); if (updated !== source) { From 6f42d75b73cb63a503118933981e9341cce80171 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 23 Oct 2026 17:22:33 +0530 Subject: [PATCH 09/10] fix: update commercial license link in contributing guide --- docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index 2343d627..f5e0b0d8 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -131,6 +131,6 @@ Before your PR can be merged, you'll need to sign our CLA: BentoPDF is dual-licensed: - **AGPL-3.0** for open-source projects where you share your full source code publicly -- **Commercial License** for proprietary/closed-source applications - [Get Lifetime License for $79](https://ko-fi.com/s/f32ca4cb75) +- **Commercial License** for proprietary/closed-source applications - [Get Lifetime License for $79](https://buy.polar.sh/polar_cl_ThDfffbl733x7oAodcIryCzhlO57ZtcWPq6HJ1qMChd) By contributing, you agree that your contributions will be licensed under these terms. From dafb6f8e188591bf5838728542d4cd234b8d4a75 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 23 Oct 2026 17:22:40 +0530 Subject: [PATCH 10/10] fix: update commercial license purchase link and invoicing details --- docs/licensing.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/licensing.md b/docs/licensing.md index e9e24f0b..db3d996d 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -8,7 +8,7 @@ For complete licensing information, delivery details, AGPL component notices, an ## Get a Commercial License -**[Get Lifetime License for $79](https://ko-fi.com/s/f32ca4cb75)** — A one-time purchase that includes all future updates forever. +**[Get Lifetime License for $79](https://buy.polar.sh/polar_cl_ThDfffbl733x7oAodcIryCzhlO57ZtcWPq6HJ1qMChd)**. A one-time purchase that includes all future updates forever. ## When Do You Need a Commercial License? @@ -56,10 +56,10 @@ The commercial license covers BentoPDF's own code. If you configure and use AGPL ## Invoicing -- Ko-fi does not automatically issue invoices. -- An official invoice will be provided immediately upon request. +- Payments are handled by [Polar](https://polar.sh), which automatically emails an invoice after purchase. +- Polar handles VAT invoices for businesses in applicable regions. Select **"I'm purchasing as a business"** at checkout and enter your billing address and Tax/VAT number. -**Contact us:** [contact@bentopdf.com](mailto:contact@bentopdf.com) with your purchase details. +**Questions?** Contact [contact@bentopdf.com](mailto:contact@bentopdf.com) with your purchase details. ## What's Included @@ -89,4 +89,4 @@ Components like CoherentPDF are licensed under AGPL v3 and remain under that lic ### How do I get an invoice? -Email [contact@bentopdf.com](mailto:contact@bentopdf.com) with your Ko-fi purchase confirmation, and we'll send an official invoice. +Polar emails your invoice automatically after purchase. For a VAT invoice, select "I'm purchasing as a business" at checkout. If you need anything else, email [contact@bentopdf.com](mailto:contact@bentopdf.com) with your purchase details.