Merge remote-tracking branch 'origin/main' into fix/933-site-url-blog-canonicals

This commit is contained in:
David Cumps
2026-10-23 13:54:39 +00:00
55 changed files with 4549 additions and 153 deletions
+8 -7
View File
@@ -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)
@@ -308,6 +308,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)!
@@ -809,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)
@@ -865,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
@@ -929,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`:
@@ -939,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
+2
View File
@@ -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? |
---
+4 -1
View File
@@ -332,7 +332,10 @@
<i data-lucide="arrow-right"></i>
</a>
</span>
<a href="https://bentopdf.com/docs/" class="lp-btn lp-btn--ghost">
<a
href="https://www.bentopdf.com/docs"
class="lp-btn lp-btn--ghost"
>
<i data-lucide="book-open"></i>
<span data-i18n="nav.docs">Docs</span>
</a>
+2 -2
View File
@@ -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 }])
},
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+6 -6
View File
@@ -3,12 +3,12 @@
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
**[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:** [[email protected]](mailto:[email protected]) with your purchase details.
**Questions?** Contact [[email protected]](mailto:[email protected]) 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 [[email protected]](mailto:[email protected]) 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 [[email protected]](mailto:[email protected]) with your purchase details.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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"
>
<img
src="images/gdpr.svg"
src="/images/gdpr.svg"
alt="GDPR compliance"
class="w-full h-full"
/>
@@ -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"
>
<img
src="images/ccpa.svg"
src="/images/ccpa.svg"
alt="CCPA compliance"
class="w-full h-full"
/>
@@ -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"
>
<img
src="images/hipaa.svg"
src="/images/hipaa.svg"
alt="HIPAA compliance"
class="w-full h-full"
/>
+19 -16
View File
@@ -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,36 +93,33 @@ 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-]*)/(\?.*)?$) {
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-]*)/(\?.*)?$) {
return 301 /$1$2;
if ($request_uri ~ ^(/[^?]*[^/?])/+(\?.*)?$) {
return 301 $1$2;
}
location ~ /src/pages/ {
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)(/.*)?$ {
try_files $uri $uri/ $uri.html =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|ca)(/.*)?$ {
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;
}
}
+9 -1
View File
@@ -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 المتعددة",
@@ -840,7 +848,7 @@
"subtitle": "اكتب أو الصق Markdown وصدّره كـ PDF منسق بشكل جميل.",
"paneMarkdown": "Markdown",
"panePreview": "معاينة",
"btnUpload": "رفع",
"btnUpload": "اختيار",
"btnSyncScroll": "مزامنة التمرير",
"btnSettings": "الإعدادات",
"btnExportPdf": "تصدير PDF",
+9 -1
View File
@@ -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",
@@ -842,7 +850,7 @@
"subtitle": "Напісаць або ўставіць Markdown і экспартаваць яго ў прыгожа аформлены PDF.",
"paneMarkdown": "Markdown",
"panePreview": "Папярэдні прагляд",
"btnUpload": "Запампаваць",
"btnUpload": "Выбраць",
"btnSyncScroll": "Сінхранізацыя пракруткі",
"btnSettings": "Налады",
"btnExportPdf": "Экспартаваць PDF",
+390
View File
@@ -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?<br><br>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"
}
}
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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",
@@ -856,7 +864,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+390
View File
@@ -0,0 +1,390 @@
{
"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": "آیا مطمئنید می‌خواهید همهٔ میان‌برها را به حالت پیش‌فرض بازنشانی کنید؟<br><br>این عمل قابل بازگشت نیست.",
"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": "ما را دنبال کنید",
"privacyShort": "حریم خصوصی",
"termsShort": "شرایط"
},
"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": "مأموریت",
"titleDim": "ما",
"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": "بازگشت به خانه"
}
}
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -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",
@@ -855,7 +863,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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マルチツール",
@@ -840,7 +848,7 @@
"subtitle": "Markdownを記述または貼り付けて、見栄えの良いPDFとしてエクスポートします。",
"paneMarkdown": "マークダウン",
"panePreview": "プレビュー",
"btnUpload": "アップロード",
"btnUpload": "選択",
"btnSyncScroll": "同期スクロール",
"btnSettings": "設定",
"btnExportPdf": "PDFをエクスポート",
+9 -1
View File
@@ -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 멀티 도구",
@@ -840,7 +848,7 @@
"subtitle": "Markdown을 작성하거나 붙여넣고 깔끔하게 서식이 적용된 PDF로 내보냅니다.",
"paneMarkdown": "Markdown",
"panePreview": "미리보기",
"btnUpload": "업로드",
"btnUpload": "선택",
"btnSyncScroll": "스크롤 동기화",
"btnSettings": "설정",
"btnExportPdf": "PDF 내보내기",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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",
@@ -677,7 +685,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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 мультиинструмент",
@@ -840,7 +848,7 @@
"subtitle": "Написать или вставить Markdown, чтобы экспортировать его в виде отформатированного PDF.",
"paneMarkdown": "Markdown",
"panePreview": "Предварительный просмотр",
"btnUpload": "Загрузить",
"btnUpload": "Выбрать",
"btnSyncScroll": "Синхронизировать прокрутку",
"btnSettings": "Настройки",
"btnExportPdf": "Экспортировать PDF",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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ç",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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 мультиінструмент",
@@ -840,7 +848,7 @@
"subtitle": "Напишіть або вставте Markdown і експортуйте його у красиво оформлений PDF.",
"paneMarkdown": "Markdown",
"panePreview": "Попередній перегляд",
"btnUpload": "Завантажити",
"btnUpload": "Вибрати",
"btnSyncScroll": "Синхронізувати прокрутку",
"btnSettings": "Налаштування",
"btnExportPdf": "Експортувати у PDF",
+9 -1
View File
@@ -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",
@@ -840,7 +848,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",
+9 -1
View File
@@ -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 多功能工具",
@@ -840,7 +848,7 @@
"subtitle": "編寫或貼上 Markdown 並匯出為精美格式的 PDF。",
"paneMarkdown": "Markdown",
"panePreview": "預覽",
"btnUpload": "上傳",
"btnUpload": "選擇",
"btnSyncScroll": "同步捲動",
"btnSettings": "設定",
"btnExportPdf": "匯出 PDF",
+9 -1
View File
@@ -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 多功能工具",
@@ -840,7 +848,7 @@
"subtitle": "编写或粘贴 Markdown 并将其导出为精美格式的 PDF。",
"paneMarkdown": "Markdown",
"panePreview": "预览",
"btnUpload": "上传",
"btnUpload": "选择",
"btnSyncScroll": "同步滚动",
"btnSettings": "设置",
"btnExportPdf": "导出 PDF",
+3 -1
View File
@@ -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] || ''}`);
}
}
+4 -4
View File
@@ -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."
/>
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="canonical" href="${SITE_URL}/blog/" />
<link rel="canonical" href="${SITE_URL}/blog" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${SITE_URL}/blog/" />
<meta property="og:url" content="${SITE_URL}/blog" />
<meta property="og:title" content="The BentoPDF Blog" />
<meta
property="og:description"
@@ -363,7 +363,7 @@ ${cards}
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'The BentoPDF Blog',
url: `${SITE_URL}/blog/`,
url: `${SITE_URL}/blog`,
description:
'Guides and honest comparisons from the maintainer of BentoPDF.',
publisher: {
+7 -5
View File
@@ -60,8 +60,7 @@ function buildUrl(langPrefix, pagePath) {
if (BASE_PATH && BASE_PATH !== '') parts.push(BASE_PATH.replace(/^\//, ''));
if (langPrefix) parts.push(langPrefix);
if (pagePath) parts.push(pagePath.replace(/^\//, ''));
const url = parts.filter(Boolean).join('/').replace(/\/+$/, '') || SITE_URL;
return langPrefix && !pagePath ? `${url}/` : url;
return parts.filter(Boolean).join('/').replace(/\/+$/, '') || SITE_URL;
}
function resolveTranslation(langResources, key) {
@@ -142,7 +141,7 @@ const BREADCRUMB_MARKER = 'data-bentopdf-breadcrumb';
const BRAND_NAME = process.env.VITE_BRAND_NAME || 'BentoPDF';
function buildLocalHomeHref(lang) {
const langSegment = lang === 'en' ? '/' : `/${lang}/`;
const langSegment = lang === 'en' ? '/' : `/${lang}`;
return `${BASE_PATH}${langSegment}`.replace(/\/+/g, '/');
}
@@ -257,7 +256,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';
applyServerTranslations(document, translations[lang]);
@@ -378,7 +377,10 @@ function processFileForLanguage(
newHref = `${BASE_PATH}/${lang}/${href}`;
}
link.setAttribute('href', newHref);
link.setAttribute(
'href',
newHref.replace(/^([^?#]*[^/?#])\/(?=$|[?#])/, '$1')
);
});
const result = dom.serialize();
+2 -2
View File
@@ -81,7 +81,7 @@ function buildUrl(lang, pageName) {
if (lang === 'en') {
return pagePath ? `${SITE_URL}/${pagePath}` : SITE_URL;
}
return pagePath ? `${SITE_URL}/${lang}/${pagePath}` : `${SITE_URL}/${lang}/`;
return pagePath ? `${SITE_URL}/${lang}/${pagePath}` : `${SITE_URL}/${lang}`;
}
function generateSitemap() {
@@ -177,7 +177,7 @@ function generateSitemap() {
.map((file) => 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 {
+2 -2
View File
@@ -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(`<loc>${siteUrl}/docs/</loc>`)) return;
if (xml.includes(`<loc>${siteUrl}/docs</loc>`)) return;
const urls = listDocsHtml(targetDocsPath, '/docs');
const entries = urls
+24 -4
View File
@@ -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, /<a[^>]+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',
+4 -2
View File
@@ -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;
+36 -11
View File
@@ -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;
}
}
}
+190
View File
@@ -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;
+17 -6
View File
@@ -28,6 +28,8 @@ export const supportedLanguages = [
'ja',
'uk',
'sk',
'ca',
'fa',
] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];
@@ -54,6 +56,8 @@ export const languageNames: Record<SupportedLanguage, string> = {
ja: '日本語',
uk: 'Українська',
sk: 'Slovenčina',
ca: 'Català',
fa: 'فارسی',
};
export const getLanguageFromUrl = (): SupportedLanguage => {
@@ -69,7 +73,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|fa)(?:\/|$)/
);
if (
langMatch &&
@@ -166,7 +170,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|fa)(\/.*)?$/
);
if (langPrefixMatch) {
pagePathWithoutLang = langPrefixMatch[2] || '/';
@@ -180,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;
@@ -229,7 +236,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 => {
@@ -261,7 +270,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|fa)(/|$)`
);
if (langPrefixRegex.test(href)) {
return;
@@ -287,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);
});
+16 -6
View File
@@ -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) {
+405 -45
View File
@@ -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<string, string> = {
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<string, ToolCategory['tools'][number]>();
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<HTMLElement>('.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<HTMLElement>(
':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<HTMLElement>('.tool-fav-btn') ??
group.nextElementSibling?.querySelector<HTMLElement>(
'.category-header'
) ??
null;
}
tile?.remove();
if (favoriteToolIds.length === 0) group.remove();
}
dom.toolGrid
.querySelectorAll<HTMLElement>('.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<HTMLElement>(
'.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<HTMLElement>('.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<HTMLElement>('.tool-fav-btn')
.forEach(updateFavoriteButton);
updateFavoritesHint();
});
}
+13
View File
@@ -14,6 +14,19 @@ export interface DocManagerPlugin {
saveAsCopy: (id: string) => Promise<Uint8Array>;
}
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 };
+1
View File
@@ -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';
+3
View File
@@ -0,0 +1,3 @@
import type { categories } from '../config/tools.ts';
export type ToolCategory = (typeof categories)[number];
+31 -1
View File
@@ -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 {
<input type="file" accept=".md,.markdown,.txt" id="mdFileInput" style="display: none;" />
<button class="md-editor-btn md-editor-btn-secondary" id="mdUpload">
<i data-lucide="upload"></i>
<span data-i18n="tools:markdownToPdf.btnUpload">Upload</span>
<span data-i18n="tools:markdownToPdf.btnUpload">Select</span>
</button>
<div class="theme-toggle">
<i data-lucide="moon" width="16" height="16"></i>
@@ -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();
+2 -2
View File
@@ -55,7 +55,7 @@
</div>
</div>
<a
href="https://www.bentopdf.com/docs/"
href="https://www.bentopdf.com/docs"
class="nav-link"
data-i18n="nav.docs"
>Docs</a
@@ -186,7 +186,7 @@
<a href="/hyper-compress" class="mobile-nav-link">Hyper Compress</a>
</div>
<a
href="https://www.bentopdf.com/docs/"
href="https://www.bentopdf.com/docs"
class="mobile-nav-link"
data-i18n="nav.docs"
>Docs</a
+91
View File
@@ -7,6 +7,97 @@
"cleanUrls": true,
"trailingSlash": false,
"redirects": [
{
"source": "/ua/change-text-color",
"destination": "/uk/text-color",
"permanent": true
},
{
"source": "/ua/src/pages/change-text-color",
"destination": "/uk/text-color",
"permanent": true
},
{
"source": "/:locale(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/src/pages/change-text-color",
"destination": "/:locale/text-color",
"permanent": true
},
{
"source": "/(en|tools|src/pages|en/src/pages)/change-text-color",
"destination": "/text-color",
"permanent": true
},
{
"source": "/ua/pdf-to-docx",
"destination": "/uk/pdf-to-word",
"permanent": true
},
{
"source": "/ua/src/pages/pdf-to-docx",
"destination": "/uk/pdf-to-word",
"permanent": true
},
{
"source": "/:locale(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/src/pages/pdf-to-docx",
"destination": "/:locale/pdf-to-word",
"permanent": true
},
{
"source": "/(en|tools|src/pages|en/src/pages)/pdf-to-docx",
"destination": "/pdf-to-word",
"permanent": true
},
{
"source": "/ua/decrypt-pdf",
"destination": "/uk/unlock-pdf",
"permanent": true
},
{
"source": "/ua/src/pages/decrypt-pdf",
"destination": "/uk/unlock-pdf",
"permanent": true
},
{
"source": "/:locale(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/src/pages/decrypt-pdf",
"destination": "/:locale/unlock-pdf",
"permanent": true
},
{
"source": "/(en|tools|src/pages|en/src/pages)/decrypt-pdf",
"destination": "/unlock-pdf",
"permanent": true
},
{
"source": "/ua/encrypt-pdf",
"destination": "/uk/protect-pdf",
"permanent": true
},
{
"source": "/ua/src/pages/encrypt-pdf",
"destination": "/uk/protect-pdf",
"permanent": true
},
{
"source": "/:locale(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/src/pages/encrypt-pdf",
"destination": "/:locale/protect-pdf",
"permanent": true
},
{
"source": "/(en|tools|src/pages|en/src/pages)/encrypt-pdf",
"destination": "/protect-pdf",
"permanent": true
},
{ "source": "/ua", "destination": "/uk", "permanent": true },
{
"source": "/ua/src/pages/:page",
"destination": "/uk/:page",
"permanent": true
},
{
"source": "/en/src/pages/:page",
"destination": "/:page",
"permanent": true
},
{ "source": "/ua/:path*", "destination": "/uk/:path*", "permanent": true },
{ "source": "/en/:path*", "destination": "/:path*", "permanent": true },
{
+11 -2
View File
@@ -49,6 +49,8 @@ const SUPPORTED_LANGUAGES = [
'ja',
'uk',
'sk',
'ca',
'fa',
] as const;
const LANG_REGEX = new RegExp(
`^/(${SUPPORTED_LANGUAGES.join('|')})(?:/(.*))?$`
@@ -513,11 +515,18 @@ function rewriteHtmlPathsPlugin(): Plugin {
for (const fileName of Object.keys(bundle)) {
if (!fileName.endsWith('.html')) continue;
const diskPath = resolve(outDir, fileName);
if (!fs.existsSync(diskPath)) continue;
const diskPath = [
resolve(outDir, fileName),
resolve(outDir, fileName.replace(/^src\/pages\//, '')),
].find((candidate) => 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) {