From b2e69786490021f8be32a96435f93fa7f995fc8e Mon Sep 17 00:00:00 2001 From: Alexandre Reis Date: Wed, 17 Dec 2025 23:16:58 -0300 Subject: [PATCH 01/39] feat(i18n): add pt-BR translation --- public/locales/pt/common.json | 318 ++++++++++++++++++++++++++++++++++ public/locales/pt/tools.json | 282 ++++++++++++++++++++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 public/locales/pt/common.json create mode 100644 public/locales/pt/tools.json diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json new file mode 100644 index 000000000..3d7dc6014 --- /dev/null +++ b/public/locales/pt/common.json @@ -0,0 +1,318 @@ +{ + "nav": { + "home": "Início", + "about": "Sobre", + "contact": "Contato", + "licensing": "Licenciamento", + "allTools": "Todas as Ferramentas", + "openMainMenu": "Abrir menu principal", + "language": "Idioma" + }, + "hero": { + "title": "O", + "pdfToolkit": "Kit de Ferramentas PDF", + "builtForPrivacy": "feito para sua privacidade", + "noSignups": "Sem Cadastros", + "unlimitedUse": "Uso Ilimitado", + "worksOffline": "Funciona Offline", + "startUsing": "Comece a Usar Agora" + }, + "usedBy": { + "title": "Usado por empresas e pessoas que trabalham em" + }, + "features": { + "title": "Por que escolher o", + "bentoPdf": "BentoPDF?", + "noSignup": { + "title": "Sem Cadastro", + "description": "Comece instantaneamente, sem contas ou e-mails." + }, + "noUploads": { + "title": "Sem Uploads", + "description": "100% no navegador, seus arquivos nunca saem do seu dispositivo." + }, + "foreverFree": { + "title": "Sempre Grátis", + "description": "Todas as ferramentas, sem testes ou assinaturas." + }, + "noLimits": { + "title": "Sem Limites", + "description": "Use o quanto quiser, sem taxas escondidas." + }, + "batchProcessing": { + "title": "Processamento em Lote", + "description": "Gerencie vários PDFs de uma só vez." + }, + "lightningFast": { + "title": "Super Rápido", + "description": "Processe PDFs instantaneamente, sem esperas ou atrasos." + } + }, + "tools": { + "title": "Comece com as", + "toolsLabel": "Ferramentas", + "subtitle": "Clique em uma ferramenta para abrir o seletor de arquivos", + "searchPlaceholder": "Buscar ferramenta (ex: 'dividir', 'organizar'...)", + "backToTools": "Voltar para Ferramentas" + }, + "upload": { + "clickToSelect": "Clique para selecionar um arquivo", + "orDragAndDrop": "ou arraste e solte", + "pdfOrImages": "PDFs ou Imagens", + "filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.", + "addMore": "Adicionar Mais Arquivos", + "clearAll": "Limpar Tudo" + }, + "loader": { + "processing": "Processando..." + }, + "alert": { + "title": "Alerta", + "ok": "OK" + }, + "preview": { + "title": "Visualização do Documento", + "downloadAsPdf": "Baixar como PDF", + "close": "Fechar" + }, + "settings": { + "title": "Configurações", + "shortcuts": "Atalhos", + "preferences": "Preferências", + "displayPreferences": "Preferências de Exibição", + "searchShortcuts": "Buscar atalhos...", + "shortcutsInfo": "Mantenha as teclas pressionadas para definir um atalho. As alterações são salvas automaticamente.", + "shortcutsWarning": "⚠️ Evite atalhos comuns do navegador (Cmd/Ctrl+W, T, N etc.), pois podem não funcionar corretamente.", + "import": "Importar", + "export": "Exportar", + "resetToDefaults": "Restaurar Padrões", + "fullWidthMode": "Modo Largura Total", + "fullWidthDescription": "Usa toda a largura da tela para as ferramentas em vez de um container centralizado", + "settingsAutoSaved": "As configurações são salvas automaticamente", + "clickToSet": "Clique para definir", + "pressKeys": "Pressione as teclas...", + "warnings": { + "alreadyInUse": "Atalho Já em Uso", + "assignedTo": "já está atribuído a:", + "chooseDifferent": "Por favor, escolha um atalho diferente.", + "reserved": "Aviso de Atalho Reservado", + "commonlyUsed": "é comumente usado para:", + "unreliable": "Este atalho pode não funcionar bem ou conflitar com o navegador/sistema.", + "useAnyway": "Deseja usar mesmo assim?", + "resetTitle": "Redefinir Atalhos", + "resetMessage": "Tem certeza que deseja redefinir todos os atalhos?

Esta ação não pode ser desfeita.", + "importSuccessTitle": "Importação Concluída", + "importSuccessMessage": "Atalhos importados com sucesso!", + "importFailTitle": "Falha na Importação", + "importFailMessage": "Falha ao importar atalhos. Formato de arquivo inválido." + } + }, + "warning": { + "title": "Aviso", + "cancel": "Cancelar", + "proceed": "Prosseguir" + }, + "compliance": { + "title": "Seus dados nunca saem do seu dispositivo", + "weKeep": "Mantemos", + "yourInfoSafe": "suas informações seguras", + "byFollowingStandards": "seguindo padrões globais de segurança.", + "processingLocal": "Todo o processamento acontece localmente no seu dispositivo.", + "gdpr": { + "title": "Conformidade GDPR", + "description": "Protege os dados pessoais e a privacidade de indivíduos na União Europeia." + }, + "ccpa": { + "title": "Conformidade CCPA", + "description": "Dá aos residentes da Califórnia direitos sobre como suas informações pessoais são coletadas e usadas." + }, + "hipaa": { + "title": "Conformidade HIPAA", + "description": "Estabelece salvaguardas para o tratamento de informações de saúde sensíveis nos Estados Unidos." + } + }, + "faq": { + "title": "Perguntas", + "questions": "Frequentes", + "isFree": { + "question": "O BentoPDF é realmente grátis?", + "answer": "Sim, com certeza. Todas as ferramentas do BentoPDF são 100% gratuitas, sem limites de arquivos, sem cadastros e sem marcas d'água. Acreditamos que todos merecem acesso a ferramentas PDF poderosas sem barreiras financeiras." + }, + "areFilesSecure": { + "question": "Meus arquivos estão seguros? Onde são processados?", + "answer": "Seus arquivos estão o mais seguros possível porque nunca saem do seu computador. Todo o processamento ocorre diretamente no seu navegador (client-side). Nunca fazemos upload para um servidor, garantindo privacidade total." + }, + "platforms": { + "question": "Funciona no Mac, Windows e Celular?", + "answer": "Sim! Como o BentoPDF roda inteiramente no navegador, funciona em qualquer sistema operacional moderno, incluindo Windows, macOS, Linux, iOS e Android." + }, + "gdprCompliant": { + "question": "O BentoPDF está em conformidade com a GDPR?", + "answer": "Sim. Como o processamento é local e não coletamos seus arquivos, não temos acesso aos seus dados. Isso garante total conformidade e controle por parte do usuário." + }, + "dataStorage": { + "question": "Vocês armazenam ou rastreiam meus arquivos?", + "answer": "Não. Nunca armazenamos, rastreamos ou registramos seus arquivos. Tudo acontece na memória do navegador e desaparece ao fechar a página. Não há logs nem servidores envolvidos." + }, + "different": { + "question": "O que torna o BentoPDF diferente de outras ferramentas?", + "answer": "A maioria das ferramentas faz upload dos arquivos para um servidor. O BentoPDF usa tecnologia web moderna para processar tudo localmente no seu navegador, garantindo mais velocidade e privacidade." + }, + "browserBased": { + "question": "Como o processamento no navegador me mantém seguro?", + "answer": "Ao rodar no seu dispositivo, eliminamos riscos de ataques a servidores ou vazamentos de dados de terceiros. Seus arquivos permanecem seus — sempre." + }, + "analytics": { + "question": "Vocês usam cookies ou rastreamento?", + "answer": "Usamos apenas o Simple Analytics para contar visitas de forma anônima. Sabemos quantos usuários nos visitam, mas nunca quem você é. O sistema respeita totalmente a GDPR." + } + }, + "testimonials": { + "title": "O que nossos", + "users": "Usuários", + "say": "Dizem" + }, + "support": { + "title": "Gostou do Trabalho?", + "description": "O BentoPDF é um projeto pessoal feito para fornecer ferramentas poderosas e privadas para todos. Se for útil para você, considere apoiar o desenvolvimento!", + "buyMeCoffee": "Pague um Café" + }, + "footer": { + "copyright": "© 2025 BentoPDF. Todos os direitos reservados.", + "version": "Versão", + "company": "Empresa", + "aboutUs": "Sobre Nós", + "faqLink": "FAQ", + "contactUs": "Contato", + "legal": "Jurídico", + "termsAndConditions": "Termos e Condições", + "privacyPolicy": "Política de Privacidade", + "followUs": "Siga-nos" + }, + "merge": { + "title": "Mesclar PDFs", + "description": "Combine arquivos inteiros ou selecione páginas específicas para criar um novo documento.", + "fileMode": "Modo Arquivo", + "pageMode": "Modo Página", + "howItWorks": "Como funciona:", + "fileModeInstructions": [ + "Clique e arraste o ícone para alterar a ordem dos arquivos.", + "No campo \"Páginas\", você pode definir intervalos (ex: \"1-3, 5\") para mesclar apenas essas páginas.", + "Deixe o campo em branco para incluir todas as páginas do arquivo." + ], + "pageModeInstructions": [ + "Todas as páginas dos PDFs enviados aparecem abaixo.", + "Arraste as miniaturas para criar a ordem exata que deseja no novo arquivo." + ], + "mergePdfs": "Mesclar PDFs" + }, + "common": { + "page": "Página", + "pages": "Páginas", + "of": "de", + "download": "Baixar", + "cancel": "Cancelar", + "save": "Salvar", + "delete": "Excluir", + "edit": "Editar", + "add": "Adicionar", + "remove": "Remover", + "loading": "Carregando...", + "error": "Erro", + "success": "Sucesso", + "file": "Arquivo", + "files": "Arquivos" + }, + "about": { + "hero": { + "title": "Acreditamos que ferramentas PDF devem ser", + "subtitle": "rápidas, privadas e gratuitas.", + "noCompromises": "Sem concessões." + }, + "mission": { + "title": "Nossa Missão", + "description": "Fornecer o kit de ferramentas PDF mais completo, respeitando sua privacidade e sem cobrar por isso. Ferramentas essenciais devem ser acessíveis a todos, sem barreiras." + }, + "philosophy": { + "label": "Nossa Filosofia", + "title": "Privacidade Primeiro. Sempre.", + "description": "Em uma era onde dados são mercadoria, seguimos outro caminho. Todo o processamento ocorre no seu navegador. Arquivos não tocam nossos servidores e não rastreamos você. Privacidade não é apenas um recurso; é nossa base." + }, + "whyBentopdf": { + "title": "Por que o BentoPDF?", + "speed": { + "title": "Feito para Velocidade", + "description": "Sem esperas de upload. Usando tecnologias como WebAssembly, processamos tudo diretamente no navegador com velocidade inigualável." + }, + "free": { + "title": "Totalmente Grátis", + "description": "Sem períodos de teste, assinaturas ou funções \"premium\" bloqueadas. Acreditamos em ferramentas como um serviço público." + }, + "noAccount": { + "title": "Sem Necessidade de Conta", + "description": "Use qualquer ferramenta imediatamente. Não pedimos e-mail, senha ou qualquer dado pessoal. Seu fluxo de trabalho deve ser anônimo." + }, + "openSource": { + "title": "Espírito Open Source", + "description": "Construído com transparência. Utilizamos bibliotecas incríveis como PDF-lib e PDF.js para democratizar o acesso a ferramentas poderosas." + } + }, + "cta": { + "title": "Pronto para começar?", + "description": "Junte-se a milhares de usuários que confiam no BentoPDF. Sinta a diferença da privacidade e do desempenho.", + "button": "Explorar Ferramentas" + } + }, + "contact": { + "title": "Entre em Contato", + "subtitle": "Adoraríamos ouvir você. Se tiver dúvidas, feedback ou sugestões de recursos, não hesite em nos contatar.", + "email": "Você pode nos contatar diretamente por e-mail em:" + }, + "licensing": { + "title": "Licenciamento de", + "subtitle": "Escolha a licença que melhor atende às suas necessidades." + }, + "multiTool": { + "uploadPdfs": "Enviar PDFs", + "upload": "Enviar", + "addBlankPage": "Adicionar Página em Branco", + "edit": "Editar:", + "undo": "Desfazer", + "redo": "Refazer", + "reset": "Redefinir", + "selection": "Seleção:", + "selectAll": "Selecionar Tudo", + "deselectAll": "Desmarcar Tudo", + "rotate": "Girar:", + "rotateLeft": "Esquerda", + "rotateRight": "Direita", + "transform": "Transformar:", + "duplicate": "Duplicar", + "split": "Dividir", + "clear": "Limpar:", + "delete": "Excluir", + "download": "Baixar:", + "downloadSelected": "Baixar Selecionadas", + "exportPdf": "Exportar PDF", + "uploadPdfFiles": "Selecionar Arquivos PDF", + "dragAndDrop": "Arraste arquivos PDF aqui ou clique para selecionar", + "selectFiles": "Selecionar Arquivos", + "renderingPages": "Renderizando páginas...", + "actions": { + "duplicatePage": "Duplicar esta página", + "deletePage": "Excluir esta página", + "insertPdf": "Inserir PDF após esta página", + "toggleSplit": "Alternar divisão após esta página" + }, + "pleaseWait": "Aguarde", + "pagesRendering": "As páginas ainda estão sendo renderizadas. Por favor, aguarde...", + "noPagesSelected": "Nenhuma Página Selecionada", + "selectOnePage": "Selecione pelo menos uma página para baixar.", + "noPages": "Sem Páginas", + "noPagesToExport": "Não há páginas para exportar.", + "renderingTitle": "Renderizando visualizações das páginas", + "errorRendering": "Falha ao renderizar miniaturas das páginas", + "error": "Erro", + "failedToLoad": "Falha ao carregar" + } +} diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json new file mode 100644 index 000000000..078194cd4 --- /dev/null +++ b/public/locales/pt/tools.json @@ -0,0 +1,282 @@ +{ + "categories": { + "popularTools": "Ferramentas Populares", + "editAnnotate": "Editar e Anotar", + "convertToPdf": "Converter para PDF", + "convertFromPdf": "Converter de PDF", + "organizeManage": "Organizar e Gerenciar", + "optimizeRepair": "Otimizar e Reparar", + "securePdf": "Segurança de PDF" + }, + "pdfMultiTool": { + "name": "Multiferramenta PDF", + "subtitle": "Mesclar, dividir, organizar, excluir, girar, adicionar páginas em branco, extrair e duplicar em uma única interface." + }, + "mergePdf": { + "name": "Mesclar PDF", + "subtitle": "Combine vários PDFs em um único arquivo. Preserva os favoritos (bookmarks)." + }, + "splitPdf": { + "name": "Dividir PDF", + "subtitle": "Extraia um intervalo de páginas para um novo PDF." + }, + "compressPdf": { + "name": "Comprimir PDF", + "subtitle": "Reduza o tamanho do arquivo do seu PDF." + }, + "pdfEditor": { + "name": "Editor de PDF", + "subtitle": "Anotar, destacar, redigir, comentar, adicionar formas/imagens, pesquisar e visualizar PDFs." + }, + "jpgToPdf": { + "name": "JPG para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens JPG." + }, + "signPdf": { + "name": "Assinar PDF", + "subtitle": "Desenhe, digite ou faça upload da sua assinatura." + }, + "cropPdf": { + "name": "Cortar PDF", + "subtitle": "Corte as margens de cada página do seu PDF." + }, + "extractPages": { + "name": "Extrair Páginas", + "subtitle": "Salve uma seleção de páginas como novos arquivos." + }, + "duplicateOrganize": { + "name": "Duplicar e Organizar", + "subtitle": "Duplique, reordene e exclua páginas." + }, + "deletePages": { + "name": "Excluir Páginas", + "subtitle": "Remova páginas específicas do seu documento." + }, + "editBookmarks": { + "name": "Editar Favoritos", + "subtitle": "Adicione, edite, importe, exclua e extraia favoritos de PDF." + }, + "tableOfContents": { + "name": "Sumário", + "subtitle": "Gere uma página de sumário a partir dos favoritos do PDF." + }, + "pageNumbers": { + "name": "Números de Página", + "subtitle": "Insira números de página no seu documento." + }, + "addWatermark": { + "name": "Adicionar Marca d'Água", + "subtitle": "Carimbe texto ou uma imagem sobre as páginas do seu PDF." + }, + "headerFooter": { + "name": "Cabeçalho e Rodapé", + "subtitle": "Adicione texto no topo e no final das páginas." + }, + "invertColors": { + "name": "Inverter Cores", + "subtitle": "Crie uma versão em \"modo escuro\" do seu PDF." + }, + "backgroundColor": { + "name": "Cor de Fundo", + "subtitle": "Altere a cor de fundo do seu PDF." + }, + "changeTextColor": { + "name": "Alterar Cor do Texto", + "subtitle": "Altere a cor do texto no seu PDF." + }, + "addStamps": { + "name": "Adicionar Carimbos", + "subtitle": "Adicione carimbos de imagem ao seu PDF usando a barra de ferramentas de anotação.", + "usernameLabel": "Nome do Usuário no Carimbo", + "usernamePlaceholder": "Digite seu nome (para os carimbos)", + "usernameHint": "Este nome aparecerá nos carimbos que você criar." + }, + "removeAnnotations": { + "name": "Remover Anotações", + "subtitle": "Remova comentários, destaques e links." + }, + "pdfFormFiller": { + "name": "Preenchimento de Formulário", + "subtitle": "Preencha formulários diretamente no navegador. Também suporta formulários XFA." + }, + "createPdfForm": { + "name": "Criar Formulário PDF", + "subtitle": "Crie formulários PDF preenchíveis com campos de texto de arrastar e soltar." + }, + "removeBlankPages": { + "name": "Remover Páginas em Branco", + "subtitle": "Detecte e exclua automaticamente páginas em branco." + }, + "imageToPdf": { + "name": "Imagem para PDF", + "subtitle": "Converta JPG, PNG, WebP, BMP, TIFF, SVG, HEIC para PDF." + }, + "pngToPdf": { + "name": "PNG para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens PNG." + }, + "webpToPdf": { + "name": "WebP para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens WebP." + }, + "svgToPdf": { + "name": "SVG para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens SVG." + }, + "bmpToPdf": { + "name": "BMP para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens BMP." + }, + "heicToPdf": { + "name": "HEIC para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens HEIC." + }, + "tiffToPdf": { + "name": "TIFF para PDF", + "subtitle": "Crie um PDF a partir de uma ou mais imagens TIFF." + }, + "textToPdf": { + "name": "Texto para PDF", + "subtitle": "Converta um arquivo de texto simples (.txt) em PDF." + }, + "jsonToPdf": { + "name": "JSON para PDF", + "subtitle": "Converta arquivos JSON para o formato PDF." + }, + "pdfToJpg": { + "name": "PDF para JPG", + "subtitle": "Converta cada página do PDF em uma imagem JPG." + }, + "pdfToPng": { + "name": "PDF para PNG", + "subtitle": "Converta cada página do PDF em uma imagem PNG." + }, + "pdfToWebp": { + "name": "PDF para WebP", + "subtitle": "Converta cada página do PDF em uma imagem WebP." + }, + "pdfToBmp": { + "name": "PDF para BMP", + "subtitle": "Converta cada página do PDF em uma imagem BMP." + }, + "pdfToTiff": { + "name": "PDF para TIFF", + "subtitle": "Converta cada página do PDF em uma imagem TIFF." + }, + "pdfToGreyscale": { + "name": "PDF para Tons de Cinza", + "subtitle": "Converta todas as cores para preto e branco." + }, + "pdfToJson": { + "name": "PDF para JSON", + "subtitle": "Converta arquivos PDF para o formato JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Torne um PDF pesquisável e copiável (reconhecimento de texto)." + }, + "alternateMix": { + "name": "Alternar e Misturar Páginas", + "subtitle": "Mescle PDFs alternando as páginas de cada arquivo. Preserva os favoritos." + }, + "addAttachments": { + "name": "Adicionar Anexos", + "subtitle": "Incorpore um ou mais arquivos dentro do seu PDF." + }, + "extractAttachments": { + "name": "Extrair Anexos", + "subtitle": "Extraia todos os arquivos incorporados de PDF(s) como um ZIP." + }, + "editAttachments": { + "name": "Editar Anexos", + "subtitle": "Visualize ou remova anexos do seu PDF." + }, + "dividePages": { + "name": "Dividir Páginas", + "subtitle": "Divida as páginas horizontalmente ou verticalmente." + }, + "addBlankPage": { + "name": "Adicionar Página em Branco", + "subtitle": "Insira uma página vazia em qualquer lugar do seu PDF." + }, + "reversePages": { + "name": "Inverter Páginas", + "subtitle": "Inverta a ordem de todas as páginas do seu documento." + }, + "rotatePdf": { + "name": "Girar PDF", + "subtitle": "Gire as páginas em incrementos de 90 graus." + }, + "nUpPdf": { + "name": "PDF N-Up", + "subtitle": "Organize várias páginas em uma única folha de impressão." + }, + "combineToSinglePage": { + "name": "Combinar em Página Única", + "subtitle": "Costure todas as páginas em um único fluxo contínuo." + }, + "viewMetadata": { + "name": "Ver Metadados", + "subtitle": "Inspecione as propriedades ocultas do seu PDF." + }, + "editMetadata": { + "name": "Editar Metadados", + "subtitle": "Altere o autor, título e outras propriedades." + }, + "pdfsToZip": { + "name": "PDFs para ZIP", + "subtitle": "Empacote vários arquivos PDF em um arquivo compactado ZIP." + }, + "comparePdfs": { + "name": "Comparar PDFs", + "subtitle": "Compare dois PDFs lado a lado." + }, + "posterizePdf": { + "name": "Posterizar PDF", + "subtitle": "Divida uma página grande em várias páginas menores." + }, + "fixPageSize": { + "name": "Ajustar Tamanho da Página", + "subtitle": "Padronize todas as páginas para um tamanho uniforme." + }, + "linearizePdf": { + "name": "Linearizar PDF", + "subtitle": "Otimize o PDF para visualização rápida na web." + }, + "pageDimensions": { + "name": "Dimensões da Página", + "subtitle": "Analise o tamanho, orientação e unidades das páginas." + }, + "removeRestrictions": { + "name": "Remover Restrições", + "subtitle": "Remova proteção por senha e restrições de segurança de arquivos assinados digitalmente." + }, + "repairPdf": { + "name": "Reparar PDF", + "subtitle": "Recupere dados de arquivos PDF corrompidos ou danificados." + }, + "encryptPdf": { + "name": "Criptografar PDF", + "subtitle": "Bloqueie seu PDF adicionando uma senha." + }, + "sanitizePdf": { + "name": "Sanitizar PDF", + "subtitle": "Remova metadados, anotações, scripts e outros dados ocultos." + }, + "decryptPdf": { + "name": "Descriptografar PDF", + "subtitle": "Desbloqueie o PDF removendo a proteção por senha." + }, + "flattenPdf": { + "name": "Achatar PDF (Flatten)", + "subtitle": "Torne os campos de formulário e anotações não editáveis." + }, + "removeMetadata": { + "name": "Remover Metadados", + "subtitle": "Limpe dados ocultos do seu PDF." + }, + "changePermissions": { + "name": "Alterar Permissões", + "subtitle": "Defina ou altere as permissões de usuário em um PDF." + } +} From 33e558b2902b5e250aa03e2fa8bbace5a91ee773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Tekg=C3=BCl?= Date: Fri, 19 Dec 2025 18:17:04 +0300 Subject: [PATCH 02/39] add tr lang --- nginx.conf | 2 +- public/locales/tr/common.json | 318 ++++++++++++++++++++++++++++++++++ public/locales/tr/tools.json | 282 ++++++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 13 +- vite.config.ts | 2 +- 5 files changed, 609 insertions(+), 8 deletions(-) create mode 100644 public/locales/tr/common.json create mode 100644 public/locales/tr/tools.json diff --git a/nginx.conf b/nginx.conf index 23dc8a7da..617498e19 100644 --- a/nginx.conf +++ b/nginx.conf @@ -23,7 +23,7 @@ http { root /usr/share/nginx/html; index index.html; - rewrite ^/(en|de|zh|vi)/(.*)$ /$2 last; + rewrite ^/(en|de|zh|vi|tr)/(.*)$ /$2 last; location / { try_files $uri $uri/ $uri.html /index.html; diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json new file mode 100644 index 000000000..ee36845ee --- /dev/null +++ b/public/locales/tr/common.json @@ -0,0 +1,318 @@ +{ + "nav": { + "home": "Ana Sayfa", + "about": "Hakkımızda", + "contact": "İletişim", + "licensing": "Lisanslama", + "allTools": "Tüm Araçlar", + "openMainMenu": "Ana menüyü aç", + "language": "Dil" + }, + "hero": { + "title": " ", + "pdfToolkit": "PDF Toolkit", + "builtForPrivacy": "gizlilik için tasarlandı", + "noSignups": "Kayıt Gerekmez", + "unlimitedUse": "Sınırsız Kullanım", + "worksOffline": "Çevrimdışı Çalışır", + "startUsing": "Hemen Kullanmaya Başla" + }, + "usedBy": { + "title": "Şu şirketler ve çalışanları tarafından kullanılıyor" + }, + "features": { + "title": "Neden", + "bentoPdf": "BentoPDF'yi seçmelisiniz?", + "noSignup": { + "title": "Kayıt Gerekmez", + "description": "Hemen başlayın, hesap veya e-posta gerekmez." + }, + "noUploads": { + "title": "Yükleme Yok", + "description": "%100 istemci tarafında çalışır, dosyalarınız cihazınızı asla terk etmez." + }, + "foreverFree": { + "title": "Tamamen Ücretsiz", + "description": "Tüm araçlar, deneme sürümü yok, ödeme duvarı yok." + }, + "noLimits": { + "title": "Sınırsız", + "description": "İstediğiniz kadar kullanın, gizli sınırlar yok." + }, + "batchProcessing": { + "title": "Toplu İşlem", + "description": "Sınırsız sayıda PDF'yi tek seferde işleyin." + }, + "lightningFast": { + "title": "Şimşek Hızında", + "description": "PDF'leri anında işleyin, bekleme veya gecikme olmadan." + } + }, + "tools": { + "title": "Araçlarla", + "toolsLabel": "Başlayın", + "subtitle": "Dosya yükleyiciyi açmak için bir araç seçin", + "searchPlaceholder": "Bir araç arayın (örn. 'böl', 'düzenle'...)", + "backToTools": "Araçlara Dön" + }, + "upload": { + "clickToSelect": "Dosya seçmek için tıklayın", + "orDragAndDrop": "veya sürükleyip bırakın", + "pdfOrImages": "PDF veya Görseller", + "filesNeverLeave": "Dosyalarınız cihazınızı asla terk etmez.", + "addMore": "Daha Fazla Dosya Ekle", + "clearAll": "Tümünü Temizle" + }, + "loader": { + "processing": "İşleniyor..." + }, + "alert": { + "title": "Uyarı", + "ok": "Tamam" + }, + "preview": { + "title": "Belge Önizleme", + "downloadAsPdf": "PDF Olarak İndir", + "close": "Kapat" + }, + "settings": { + "title": "Ayarlar", + "shortcuts": "Kısayollar", + "preferences": "Tercihler", + "displayPreferences": "Görüntü Tercihleri", + "searchShortcuts": "Kısayollarda ara...", + "shortcutsInfo": "Bir kısayol atamak için tuşlara basılı tutun. Değişiklikler otomatik olarak kaydedilir.", + "shortcutsWarning": "⚠️ Güvenilir çalışmayabileceğinden yaygın tarayıcı kısayollarından (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N vb.) kaçının.", + "import": "İçe Aktar", + "export": "Dışa Aktar", + "resetToDefaults": "Varsayılanlara Sıfırla", + "fullWidthMode": "Tam Genişlik Modu", + "fullWidthDescription": "Ortalanmış bir konteyner yerine tüm ekran genişliğini kullan", + "settingsAutoSaved": "Ayarlar otomatik olarak kaydedildi", + "clickToSet": "Ayarlamak için tıklayın", + "pressKeys": "Tuşlara basın...", + "warnings": { + "alreadyInUse": "Kısayol Zaten Kullanımda", + "assignedTo": "zaten şurada kullanılıyor:", + "chooseDifferent": "Lütfen farklı bir kısayol seçin.", + "reserved": "Ayrılmış Kısayol Uyarısı", + "commonlyUsed": "genellikle şunun için kullanılır:", + "unreliable": "Bu kısayol güvenilir çalışmayabilir veya tarayıcı/sistem davranışıyla çakışabilir.", + "useAnyway": "Yine de kullanmak istiyor musunuz?", + "resetTitle": "Kısayolları Sıfırla", + "resetMessage": "Tüm kısayolları varsayılan ayarlara sıfırlamak istediğinizden emin misiniz?

Bu işlem geri alınamaz.", + "importSuccessTitle": "İçe Aktarma Başarılı", + "importSuccessMessage": "Kısayollar başarıyla içe aktarıldı!", + "importFailTitle": "İçe Aktarma Başarısız", + "importFailMessage": "Kısayollar içe aktarılamadı. Geçersiz dosya biçimi." + } + }, + "warning": { + "title": "Uyarı", + "cancel": "İptal", + "proceed": "Devam Et" + }, + "compliance": { + "title": "Verileriniz cihazınızı asla terk etmez", + "weKeep": "Bilgilerinizi", + "yourInfoSafe": "güvende tutuyoruz", + "byFollowingStandards": "küresel güvenlik standartlarını takip ederek.", + "processingLocal": "Tüm işlemler cihazınızda yerel olarak gerçekleşir.", + "gdpr": { + "title": "GDPR uyumluluğu", + "description": "Avrupa Birliği'ndeki bireylerin kişisel verilerini ve gizliliğini korur." + }, + "ccpa": { + "title": "CCPA uyumluluğu", + "description": "Kaliforniya sakinlerine kişisel bilgilerinin nasıl toplandığı, kullanıldığı ve paylaşıldığı konusunda haklar tanır." + }, + "hipaa": { + "title": "HIPAA uyumluluğu", + "description": "ABD sağlık sisteminde hassas sağlık bilgilerinin işlenmesi için güvenlik önlemleri belirler." + } + }, + "faq": { + "title": "Sıkça Sorulan", + "questions": "Sorular", + "isFree": { + "question": "BentoPDF gerçekten ücretsiz mi?", + "answer": "Evet, kesinlikle. BentoPDF'deki tüm araçlar %100 ücretsizdir, dosya sınırı yoktur, kayıt gerekmez ve filigran eklenmez. Herkesin ödeme duvarı olmadan basit, güçlü PDF araçlarına erişimi hak ettiğine inanıyoruz." + }, + "areFilesSecure": { + "question": "Dosyalarım güvende mi? Nerede işleniyorlar?", + "answer": "Dosyalarınız mümkün olan en güvenli şekildedir çünkü bilgisayarınızı asla terk etmezler. Tüm işlemler doğrudan web tarayıcınızda (istemci tarafında) gerçekleşir. Dosyalarınızı asla bir sunucuya yüklemeyiz, böylece gizliliğiniz ve belgeleriniz üzerindeki kontrolünüz tam olarak sizde kalır." + }, + "platforms": { + "question": "Mac, Windows ve Mobil'de çalışıyor mu?", + "answer": "Evet! BentoPDF tamamen tarayıcınızda çalıştığı için, Windows, macOS, Linux, iOS ve Android dahil modern bir web tarayıcısı olan herhangi bir işletim sisteminde çalışır." + }, + "gdprCompliant": { + "question": "BentoPDF GDPR uyumlu mu?", + "answer": "Evet. BentoPDF tamamen GDPR uyumludur. Tüm dosya işlemleri tarayıcınızda yerel olarak gerçekleştiği ve dosyalarınızı herhangi bir sunucuya asla iletmediğimiz için verilerinize erişimimiz yoktur. Bu, belgeleriniz üzerindeki kontrolün her zaman sizde olduğundan emin olur." + }, + "dataStorage": { + "question": "Dosyalarımı saklıyor veya takip ediyor musunuz?", + "answer": "Hayır. Dosyalarınızı asla saklamıyor, takip etmiyor veya kaydetmiyoruz. BentoPDF'de yaptığınız her şey tarayıcı belleğinizde gerçekleşir ve sayfayı kapattığınızda silinir. Yükleme, geçmiş kaydı veya sunucu yoktur." + }, + "different": { + "question": "BentoPDF'yi diğer PDF araçlarından farklı kılan nedir?", + "answer": "Çoğu PDF aracı, işlem için dosyalarınızı bir sunucuya yükler. BentoPDF asla böyle yapmaz. Dosyalarınızı doğrudan tarayıcınızda işlemek için güvenli, modern web teknolojileri kullanırız. Bu, daha hızlı performans, daha güçlü gizlilik ve tam bir gönül rahatlığı anlamına gelir." + }, + "browserBased": { + "question": "Tarayıcı tabanlı işlem beni nasıl korur?", + "answer": "Tamamen tarayıcınızın içinde çalışarak, BentoPDF dosyalarınızın cihazınızı asla terk etmemesini sağlar. Bu, sunucu saldırıları, veri ihlalleri veya yetkisiz erişim risklerini ortadan kaldırır. Dosyalarınız her zaman sizin kalır." + }, + "analytics": { + "question": "Beni takip etmek için çerez veya analiz kullanıyor musunuz?", + "answer": "Gizliliğinizi önemsiyoruz. BentoPDF kişisel bilgileri takip etmez. Sadece anonim ziyaretçi sayılarını görmek için Simple Analytics kullanıyoruz. Bu, sitemizi kaç kişinin ziyaret ettiğini görebileceğimiz, ancak kim olduğunuzu asla bilemeyeceğimiz anlamına gelir. Simple Analytics tamamen GDPR uyumludur ve gizliliğinize saygı gösterir." + } + }, + "testimonials": { + "title": "Kullanıcılarımız", + "users": "Ne Diyor", + "say": "" + }, + "support": { + "title": "Çalışmamı Beğendiniz mi?", + "description": "BentoPDF, herkes için ücretsiz, özel ve güçlü bir PDF araç seti sağlamak amacıyla oluşturulmuş bir tutku projesidir. Faydalı bulduysanız, geliştirilmesini desteklemeyi düşünebilirsiniz. Her kahve yardımcı olur!", + "buyMeCoffee": "Bana Kahve Ismarla" + }, + "footer": { + "copyright": "© 2025 BentoPDF. Tüm hakları saklıdır.", + "version": "Sürüm", + "company": "Şirket", + "aboutUs": "Hakkımızda", + "faqLink": "SSS", + "contactUs": "İletişim", + "legal": "Yasal", + "termsAndConditions": "Kullanım Koşulları", + "privacyPolicy": "Gizlilik Politikası", + "followUs": "Bizi Takip Edin" + }, + "merge": { + "title": "PDF Birleştir", + "description": "Dosyaların tamamını birleştirin veya yeni bir belge oluşturmak için belirli sayfaları seçin.", + "fileMode": "Dosya Modu", + "pageMode": "Sayfa Modu", + "howItWorks": "Nasıl Çalışır:", + "fileModeInstructions": [ + "Dosyaların sırasını değiştirmek için simgeyi tıklayıp sürükleyin.", + "Her dosya için \"Sayfalar\" kutusuna, yalnızca o sayfaları birleştirmek için aralıklar belirtebilirsiniz (örn. \"1-3, 5\").", + "Tüm sayfaları dahil etmek için \"Sayfalar\" kutusunu boş bırakın." + ], + "pageModeInstructions": [ + "Yüklediğiniz PDF'lerin tüm sayfaları aşağıda gösterilmiştir.", + "Yeni dosyanız için istediğiniz sırayı oluşturmak üzere sayfa küçük resimlerini sürükleyip bırakmanız yeterlidir." + ], + "mergePdfs": "PDF'leri Birleştir" + }, + "common": { + "page": "Sayfa", + "pages": "Sayfa", + "of": "- ", + "download": "İndir", + "cancel": "İptal", + "save": "Kaydet", + "delete": "Sil", + "edit": "Düzenle", + "add": "Ekle", + "remove": "Kaldır", + "loading": "Yükleniyor...", + "error": "Hata", + "success": "Başarılı", + "file": "Dosya", + "files": "Dosya" + }, + "about": { + "hero": { + "title": "PDF araçlarının", + "subtitle": "hızlı, özel ve ücretsiz olması gerektiğine inanıyoruz.", + "noCompromises": "Taviz yok." + }, + "mission": { + "title": "Misyonumuz", + "description": "Gizliliğinize saygı duyan ve asla ödeme talep etmeyen en kapsamlı PDF araç setini sağlamak. Temel belge araçlarının her yerde, herkes için erişilebilir olması gerektiğine inanıyoruz." + }, + "philosophy": { + "label": "Temel Felsefemiz", + "title": "Öncelik Her Zaman Gizlilik.", + "description": "Verinin bir meta olarak kabul edildiği bir çağda, farklı bir yaklaşım benimsiyoruz. Bentopdf araçları için tüm işlemler tarayıcınızda yerel olarak gerçekleşir. Bu, dosyalarınızın sunucularımıza asla dokunmadığı, belgelerinizi asla görmediğimiz ve ne yaptığınızı takip etmediğimiz anlamına gelir. Belgeleriniz tamamen ve kesinlikle özel kalır. Bu sadece bir özellik değil, temelimizdir." + }, + "whyBentopdf": { + "title": "Neden", + "speed": { + "title": "Hız İçin Tasarlandı", + "description": "Sunucuya yükleme veya indirme için bekleme yok. WebAssembly gibi modern web teknolojilerini kullanarak dosyaları doğrudan tarayıcınızda işleyerek, tüm araçlarımız için benzersiz bir hız sunuyoruz." + }, + "free": { + "title": "Tamamen Ücretsiz", + "description": "Deneme sürümü yok, abonelik yok, gizli ücret yok ve \"premium\" özellikler rehin alınmamış. Güçlü PDF araçlarının bir kar merkezi değil, bir kamu hizmeti olması gerektiğine inanıyoruz." + }, + "noAccount": { + "title": "Hesap Gerekmez", + "description": "Hemen herhangi bir aracı kullanmaya başlayın. E-postanıza, şifrenize veya herhangi bir kişisel bilginize ihtiyacımız yok. İş akışınız sürtünmesiz ve anonim olmalıdır." + }, + "openSource": { + "title": "Açık Kaynak Ruhu", + "description": "Şeffaflık düşünülerek oluşturuldu. PDF-lib ve PDF.js gibi inanılmaz açık kaynaklı kütüphanelerden yararlanıyoruz ve güçlü araçları herkes için erişilebilir kılmak için topluluk odaklı çabaya inanıyoruz." + } + }, + "cta": { + "title": "Başlamaya hazır mısınız?", + "description": "Günlük belge ihtiyaçları için BentoPDF'ye güvenen binlerce kullanıcıya katılın. Gizlilik ve performansın yaratabileceği farkı deneyimleyin.", + "button": "Tüm Araçları Keşfet" + } + }, + "contact": { + "title": "İletişime Geçin", + "subtitle": "Sizden haber almak isteriz. Bir sorunuz, geri bildiriminiz veya bir özellik isteğiniz varsa, lütfen bize ulaşmaktan çekinmeyin.", + "email": "Bize doğrudan şu e-posta adresinden ulaşabilirsiniz:" + }, + "licensing": { + "title": "Lisanslama", + "subtitle": "İhtiyaçlarınıza uygun lisansı seçin." + }, + "multiTool": { + "uploadPdfs": "PDF Yükle", + "upload": "Yükle", + "addBlankPage": "Boş Sayfa Ekle", + "edit": "Düzenle:", + "undo": "Geri Al", + "redo": "Yinele", + "reset": "Sıfırla", + "selection": "Seçim:", + "selectAll": "Tümünü Seç", + "deselectAll": "Seçimi Kaldır", + "rotate": "Döndür:", + "rotateLeft": "Sola", + "rotateRight": "Sağa", + "transform": "Dönüştür:", + "duplicate": "Çoğalt", + "split": "Böl", + "clear": "Temizle:", + "delete": "Sil", + "download": "İndir:", + "downloadSelected": "Seçilenleri İndir", + "exportPdf": "PDF Olarak Dışa Aktar", + "uploadPdfFiles": "PDF Dosyalarını Seçin", + "dragAndDrop": "PDF dosyalarını buraya sürükleyip bırakın veya seçmek için tıklayın", + "selectFiles": "Dosya Seç", + "renderingPages": "Sayfalar oluşturuluyor...", + "actions": { + "duplicatePage": "Bu sayfayı çoğalt", + "deletePage": "Bu sayfayı sil", + "insertPdf": "Bu sayfadan sonra PDF ekle", + "toggleSplit": "Bu sayfadan sonra bölmeyi aç/kapat" + }, + "pleaseWait": "Lütfen Bekleyin", + "pagesRendering": "Sayfalar hala oluşturuluyor. Lütfen bekleyin...", + "noPagesSelected": "Hiçbir Sayfa Seçilmedi", + "selectOnePage": "Lütfen indirmek için en az bir sayfa seçin.", + "noPages": "Sayfa Yok", + "noPagesToExport": "Dışa aktarılacak sayfa yok.", + "renderingTitle": "Sayfa önizlemeleri oluşturuluyor", + "errorRendering": "Sayfa küçük resimleri oluşturulamadı", + "error": "Hata", + "failedToLoad": "Yüklenemedi" + } +} \ No newline at end of file diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json new file mode 100644 index 000000000..34bc151f3 --- /dev/null +++ b/public/locales/tr/tools.json @@ -0,0 +1,282 @@ +{ + "categories": { + "popularTools": "Popüler Araçlar", + "editAnnotate": "Düzenle & Açıklama Ekle", + "convertToPdf": "PDF'ye Dönüştür", + "convertFromPdf": "PDF'den Dönüştür", + "organizeManage": "Düzenle & Yönet", + "optimizeRepair": "Optimize Et & Onar", + "securePdf": "PDF Güvenliği" + }, + "pdfMultiTool": { + "name": "PDF Çoklu Araç", + "subtitle": "Birleştir, Böl, Düzenle, Sil, Döndür, Boş Sayfa Ekle, Çıkar ve Çoğalt işlemlerini tek bir arayüzde yapın." + }, + "mergePdf": { + "name": "PDF Birleştir", + "subtitle": "Birden fazla PDF'yi tek bir dosyada birleştirin. Yer imlerini korur." + }, + "splitPdf": { + "name": "PDF Böl", + "subtitle": "Sayfa aralığını yeni bir PDF olarak çıkarın." + }, + "compressPdf": { + "name": "PDF Sıkıştır", + "subtitle": "PDF dosya boyutunu küçültün." + }, + "pdfEditor": { + "name": "PDF Düzenleyici", + "subtitle": "Açıklama ekleyin, vurgulayın, düzenleyin, yorum yapın, şekil/resim ekleyin, arama yapın ve PDF'leri görüntüleyin." + }, + "jpgToPdf": { + "name": "JPG'den PDF'ye", + "subtitle": "Bir veya daha fazla JPG görselinden PDF oluşturun." + }, + "signPdf": { + "name": "PDF İmzala", + "subtitle": "İmzanızı çizin, yazın veya yükleyin." + }, + "cropPdf": { + "name": "PDF Kırp", + "subtitle": "PDF'nizdeki her sayfanın kenar boşluklarını kırpın." + }, + "extractPages": { + "name": "Sayfaları Çıkar", + "subtitle": "Seçili sayfaları yeni dosyalar olarak kaydedin." + }, + "duplicateOrganize": { + "name": "Çoğalt & Düzenle", + "subtitle": "Sayfaları çoğaltın, yeniden sıralayın ve silin." + }, + "deletePages": { + "name": "Sayfaları Sil", + "subtitle": "Belgenizden belirli sayfaları kaldırın." + }, + "editBookmarks": { + "name": "Yer İşaretlerini Düzenle", + "subtitle": "PDF yer imlerini ekleyin, düzenleyin, içe aktarın, silin ve çıkarın." + }, + "tableOfContents": { + "name": "İçindekiler", + "subtitle": "PDF yer imlerinden bir içindekiler sayfası oluşturun." + }, + "pageNumbers": { + "name": "Sayfa Numaraları", + "subtitle": "Belgenize sayfa numaraları ekleyin." + }, + "addWatermark": { + "name": "Filigran Ekle", + "subtitle": "PDF sayfalarınızın üzerine metin veya görsel damgası ekleyin." + }, + "headerFooter": { + "name": "Üst Bilgi & Alt Bilgi", + "subtitle": "Sayfaların üst ve alt kısmına metin ekleyin." + }, + "invertColors": { + "name": "Renkleri Ters Çevir", + "subtitle": "PDF'niz için \"karanlık mod\" sürümü oluşturun." + }, + "backgroundColor": { + "name": "Arka Plan Rengi", + "subtitle": "PDF'nizin arka plan rengini değiştirin." + }, + "changeTextColor": { + "name": "Metin Rengini Değiştir", + "subtitle": "PDF'nizdeki metnin rengini değiştirin." + }, + "addStamps": { + "name": "Damga Ekle", + "subtitle": "Açıklama araç çubuğunu kullanarak PDF'nize damga ekleyin.", + "usernameLabel": "Kullanıcı Adı", + "usernamePlaceholder": "Adınızı girin (damgalar için)", + "usernameHint": "Bu isim oluşturduğunuz damgalarda görünecektir." + }, + "removeAnnotations": { + "name": "Açıklamaları Kaldır", + "subtitle": "Yorumları, vurguları ve bağlantıları kaldırın." + }, + "pdfFormFiller": { + "name": "PDF Form Doldurucu", + "subtitle": "Formları doğrudan tarayıcıda doldurun. XFA formlarını da destekler." + }, + "createPdfForm": { + "name": "PDF Formu Oluştur", + "subtitle": "Sürükle-bırak metin alanları ile doldurulabilir PDF formları oluşturun." + }, + "removeBlankPages": { + "name": "Boş Sayfaları Kaldır", + "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin." + }, + "imageToPdf": { + "name": "Görselden PDF'ye", + "subtitle": "JPG, PNG, WebP, BMP, TIFF, SVG, HEIC formatlarını PDF'ye dönüştürün." + }, + "pngToPdf": { + "name": "PNG'den PDF'ye", + "subtitle": "Bir veya daha fazla PNG görselinden PDF oluşturun." + }, + "webpToPdf": { + "name": "WebP'den PDF'ye", + "subtitle": "Bir veya daha fazla WebP görselinden PDF oluşturun." + }, + "svgToPdf": { + "name": "SVG'den PDF'ye", + "subtitle": "Bir veya daha fazla SVG görselinden PDF oluşturun." + }, + "bmpToPdf": { + "name": "BMP'den PDF'ye", + "subtitle": "Bir veya daha fazla BMP görselinden PDF oluşturun." + }, + "heicToPdf": { + "name": "HEIC'den PDF'ye", + "subtitle": "Bir veya daha fazla HEIC görselinden PDF oluşturun." + }, + "tiffToPdf": { + "name": "TIFF'den PDF'ye", + "subtitle": "Bir veya daha fazla TIFF görselinden PDF oluşturun." + }, + "textToPdf": { + "name": "Metinden PDF'ye", + "subtitle": "Düz metin dosyasını PDF'ye dönüştürün." + }, + "jsonToPdf": { + "name": "JSON'dan PDF'ye", + "subtitle": "JSON dosyalarını PDF formatına dönüştürün." + }, + "pdfToJpg": { + "name": "PDF'den JPG'ye", + "subtitle": "Her PDF sayfasını JPG görseline dönüştürün." + }, + "pdfToPng": { + "name": "PDF'den PNG'ye", + "subtitle": "Her PDF sayfasını PNG görseline dönüştürün." + }, + "pdfToWebp": { + "name": "PDF'den WebP'ye", + "subtitle": "Her PDF sayfasını WebP görseline dönüştürün." + }, + "pdfToBmp": { + "name": "PDF'den BMP'ye", + "subtitle": "Her PDF sayfasını BMP görseline dönüştürün." + }, + "pdfToTiff": { + "name": "PDF'den TIFF'e", + "subtitle": "Her PDF sayfasını TIFF görseline dönüştürün." + }, + "pdfToGreyscale": { + "name": "PDF'yi Gri Tonlamaya Çevir", + "subtitle": "Tüm renkleri siyah beyaza çevirin." + }, + "pdfToJson": { + "name": "PDF'den JSON'a", + "subtitle": "PDF dosyalarını JSON formatına dönüştürün." + }, + "ocrPdf": { + "name": "PDF'de OCR", + "subtitle": "PDF'yi aranabilir ve kopyalanabilir hale getirin." + }, + "alternateMix": { + "name": "Sayfaları Karıştır & Birleştir", + "subtitle": "PDF'leri her birinden sayfaları sırayla birleştirin. Yer imlerini korur." + }, + "addAttachments": { + "name": "Ek Dosya Ekle", + "subtitle": "PDF'nize bir veya daha fazla dosya ekleyin." + }, + "extractAttachments": { + "name": "Ek Dosyaları Çıkar", + "subtitle": "PDF'lerden tüm gömülü dosyaları ZIP olarak çıkarın." + }, + "editAttachments": { + "name": "Ek Dosyaları Düzenle", + "subtitle": "PDF'nizdeki ek dosyaları görüntüleyin veya kaldırın." + }, + "dividePages": { + "name": "Sayfaları Böl", + "subtitle": "Sayfaları yatay veya dikey olarak bölün." + }, + "addBlankPage": { + "name": "Boş Sayfa Ekle", + "subtitle": "PDF'nize herhangi bir yerine boş sayfa ekleyin." + }, + "reversePages": { + "name": "Sayfaları Ters Çevir", + "subtitle": "Belgenizdeki tüm sayfaların sırasını tersine çevirin." + }, + "rotatePdf": { + "name": "PDF'yi Döndür", + "subtitle": "Sayfaları 90 derecelik artışlarla döndürün." + }, + "nUpPdf": { + "name": "N'li PDF", + "subtitle": "Birden fazla sayfayı tek bir sayfaya yerleştirin." + }, + "combineToSinglePage": { + "name": "Tek Sayfada Birleştir", + "subtitle": "Tüm sayfaları tek bir sürekli kaydırılabilir sayfada birleştirin." + }, + "viewMetadata": { + "name": "Üst Veriyi Görüntüle", + "subtitle": "PDF'nizin gizli özelliklerini inceleyin." + }, + "editMetadata": { + "name": "Üst Veriyi Düzenle", + "subtitle": "Yazar, başlık ve diğer özellikleri değiştirin." + }, + "pdfsToZip": { + "name": "PDF'leri ZIP Yap", + "subtitle": "Birden fazla PDF dosyasını bir ZIP arşivinde paketleyin." + }, + "comparePdfs": { + "name": "PDF'leri Karşılaştır", + "subtitle": "İki PDF'yi yan yana karşılaştırın." + }, + "posterizePdf": { + "name": "PDF'yi Posta Boyutuna Böl", + "subtitle": "Büyük bir sayfayı birden fazla küçük sayfaya bölün." + }, + "fixPageSize": { + "name": "Sayfa Boyutunu Düzelt", + "subtitle": "Tüm sayfaları standart bir boyuta getirin." + }, + "linearizePdf": { + "name": "PDF'yi Doğrusallaştır", + "subtitle": "Hızlı web görüntüleme için PDF'yi optimize edin." + }, + "pageDimensions": { + "name": "Sayfa Boyutları", + "subtitle": "Sayfa boyutunu, yönlendirmeyi ve birimleri analiz edin." + }, + "removeRestrictions": { + "name": "Kısıtlamaları Kaldır", + "subtitle": "Dijital olarak imzalanmış PDF dosyalarıyla ilişkili şifre korumasını ve güvenlik kısıtlamalarını kaldırın." + }, + "repairPdf": { + "name": "PDF'yi Onar", + "subtitle": "Bozulmuş veya hasarlı PDF dosyalarından veri kurtarın." + }, + "encryptPdf": { + "name": "PDF'yi Şifrele", + "subtitle": "PDF'nizi şifre ekleyerek koruyun." + }, + "sanitizePdf": { + "name": "PDF'yi Temizle", + "subtitle": "Üst verileri, açıklamaları, betikleri ve daha fazlasını kaldırın." + }, + "decryptPdf": { + "name": "PDF'nin Şifresini Çöz", + "subtitle": "Şifre korumasını kaldırarak PDF'nin kilidini açın." + }, + "flattenPdf": { + "name": "PDF'yi Düzleştir", + "subtitle": "Form alanlarını ve açıklamaları düzenlenemez hale getirin." + }, + "removeMetadata": { + "name": "Üst Veriyi Kaldır", + "subtitle": "PDF'nizdeki gizli verileri temizleyin." + }, + "changePermissions": { + "name": "İzinleri Değiştir", + "subtitle": "Bir PDF üzerindeki kullanıcı izinlerini ayarlayın veya değiştirin." + } +} \ No newline at end of file diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index f29f7d644..16fb32074 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -3,7 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // Supported languages -export const supportedLanguages = ['en', 'de', 'zh', 'vi'] as const; +export const supportedLanguages = ['en', 'de', 'zh', 'vi', 'tr'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { @@ -11,11 +11,12 @@ export const languageNames: Record = { de: 'Deutsch', zh: '中文', vi: 'Tiếng Việt', + tr: 'Türkçe', }; export const getLanguageFromUrl = (): SupportedLanguage => { const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi)(?:\/|$)/); + const langMatch = path.match(/^\/(en|de|zh|vi|tr)(?:\/|$)/); if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) { return langMatch[1] as SupportedLanguage; } @@ -71,9 +72,9 @@ export const changeLanguage = (lang: SupportedLanguage): void => { const currentLang = getLanguageFromUrl(); let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi)\//)) { - newPath = currentPath.replace(/^\/(en|de|zh|vi)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|zh|vi)$/)) { + if (currentPath.match(/^\/(en|de|zh|vi|tr)\//)) { + newPath = currentPath.replace(/^\/(en|de|zh|vi|tr)\//, `/${lang}/`); + } else if (currentPath.match(/^\/(en|de|zh|vi|tr)$/)) { newPath = `/${lang}`; } else { newPath = `/${lang}${currentPath}`; @@ -135,7 +136,7 @@ export const rewriteLinks = (): void => { return; } - if (href.match(/^\/(en|de|zh|vi)\//)) { + if (href.match(/^\/(en|de|zh|vi|tr)\//)) { return; } let newHref: string; diff --git a/vite.config.ts b/vite.config.ts index 32552f50c..66f27dd2f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,7 @@ function pagesRewritePlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url?.split('?')[0] || ''; - const langMatch = url.match(/^\/(en|de|zh|vi)(\/.*)?$/); + const langMatch = url.match(/^\/(en|de|zh|vi|tr)(\/.*)?$/); if (langMatch) { const lang = langMatch[1]; const restOfPath = langMatch[2] || '/'; From 14afebb5f0657e96807fbf33c88d29fd94387335 Mon Sep 17 00:00:00 2001 From: Raul Gonzalez Date: Fri, 2 Jan 2026 07:00:27 -0600 Subject: [PATCH 03/39] Add spanish translation and update documentation with missing steps --- TRANSLATION.md | 96 ++++++- public/locales/es/common.json | 323 ++++++++++++++++++++++ public/locales/es/tools.json | 492 ++++++++++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 5 +- vite.config.ts | 2 +- 5 files changed, 901 insertions(+), 17 deletions(-) create mode 100644 public/locales/es/common.json create mode 100644 public/locales/es/tools.json diff --git a/TRANSLATION.md b/TRANSLATION.md index 00354a5da..04fb60c6b 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -34,7 +34,7 @@ The app automatically detects the language from the URL path: **To improve existing translations:** -1. Navigate to `public/locales/{language}/common.json` +1. Navigate to `public/locales/{language}/common.json` and `public/locales/{language}/tools.json` 2. Find the key you want to update 3. Change the translation value 4. Save and test @@ -42,10 +42,13 @@ The app automatically detects the language from the URL path: **To add a new language (e.g., Spanish):** 1. Copy `public/locales/en/common.json` to `public/locales/es/common.json` -2. Translate all values in `es/common.json` -3. Add Spanish to `supportedLanguages` in `src/js/i18n/i18n.ts` -4. Add Spanish name to `languageNames` in `src/js/i18n/i18n.ts` -5. Test thoroughly +2. Copy `public/locales/en/tools.json` to `public/locales/es/tools.json` +3. Translate all values in both `es/common.json` and `es/tools.json` +4. Add Spanish to `supportedLanguages` in `src/js/i18n/i18n.ts` +5. Add Spanish name to `languageNames` in `src/js/i18n/i18n.ts` +6. Add Spanish language code to the routing regex in `vite.config.ts` +7. Restart the dev server +8. Test thoroughly --- @@ -53,17 +56,18 @@ The app automatically detects the language from the URL path: Let's add **French** as an example: -### Step 1: Create Translation File +### Step 1: Create Translation Files ```bash # Create the directory mkdir -p public/locales/fr -# Copy the English template +# Copy the English templates cp public/locales/en/common.json public/locales/fr/common.json +cp public/locales/en/tools.json public/locales/fr/tools.json ``` -### Step 2: Translate the JSON File +### Step 2: Translate the JSON Files Open `public/locales/fr/common.json` and translate all the values: @@ -95,27 +99,66 @@ Open `public/locales/fr/common.json` and translate all the values: "accueil": "Accueil" ``` +Then do the same for `public/locales/fr/tools.json` to translate all tool names and descriptions. + ### Step 3: Register the Language Edit `src/js/i18n/i18n.ts`: ```typescript // Add 'fr' to supported languages -export const supportedLanguages = ['en', 'de', 'fr'] as const; +export const supportedLanguages = ['en', 'de', 'es', 'fr', 'zh', 'vi'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; // Add French display name export const languageNames: Record = { en: 'English', de: 'Deutsch', + es: 'Español', fr: 'Français', // ← Add this + zh: '中文', + vi: 'Tiếng Việt', +}; +``` + +Also update the `getLanguageFromUrl` function in the same file: + +```typescript +export const getLanguageFromUrl = (): SupportedLanguage => { + const path = window.location.pathname; + const langMatch = path.match(/^\/(en|de|es|fr|zh|vi)(?:\/|$)/); // ← Add 'fr' here + // ... rest of the function }; ``` -### Step 4: Test Your Translation +### Step 4: Update Vite Configuration + +Edit `vite.config.ts` to add French to the routing middleware: + +```typescript +function pagesRewritePlugin(): Plugin { + return { + name: 'pages-rewrite', + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = req.url?.split('?')[0] || ''; + + // Add 'fr' to this regex pattern + const langMatch = url.match(/^\/(en|de|es|fr|zh|vi)(\/.*)?$/); + // ... rest of the middleware + }); + }, + }; +} +``` + +⚠️ **Important**: This step is critical! Without updating the Vite config, you'll get 404 errors when trying to access French pages. + +### Step 5: Restart and Test Your Translation ```bash -# Start the dev server +# Stop the dev server (Ctrl+C) +# Start it again npm run dev # Visit the French version @@ -415,14 +458,34 @@ SyntaxError: Unexpected token } in JSON at position 1234 **Solution:** Make sure you added the language to both arrays in `i18n.ts`: ```typescript -export const supportedLanguages = ['en', 'de', 'fr']; // ← Add here +export const supportedLanguages = ['en', 'de', 'es', 'fr', 'zh', 'vi']; // ← Add here export const languageNames = { en: 'English', de: 'Deutsch', + es: 'Español', fr: 'Français', // ← And here + zh: '中文', + vi: 'Tiếng Việt', }; ``` +### Issue: 404 Error When Accessing Language Pages + +**Symptoms:** +Visiting `http://localhost:5173/fr/about.html` shows a 404 error page. + +**Solution:** +You need to update `vite.config.ts` to include your language code in the routing regex: +```typescript +// In the pagesRewritePlugin function +const langMatch = url.match(/^\/(en|de|es|fr|zh|vi)(\/.*)?$/); // ← Add your language code +``` + +After updating, restart the dev server: +```bash +npm run dev +``` + --- ## File Checklist @@ -430,11 +493,14 @@ export const languageNames = { When adding a new language, make sure these files are updated: - [ ] `public/locales/{lang}/common.json` - Main translation file -- [ ] `src/js/i18n/i18n.ts` - Add to `supportedLanguages` and `languageNames` +- [ ] `public/locales/{lang}/tools.json` - Tools translation file +- [ ] `src/js/i18n/i18n.ts` - Add to `supportedLanguages`, `languageNames`, and `getLanguageFromUrl` regex +- [ ] `vite.config.ts` - Add language code to routing regex in `pagesRewritePlugin` - [ ] Test all pages: homepage, about, contact, FAQ, tool pages - [ ] Test settings modal and shortcuts - [ ] Test language switcher in footer - [ ] Verify URL routing works (`/{lang}/`) +- [ ] Test that all tools load correctly --- @@ -474,9 +540,11 @@ Current translation coverage: |----------|------|--------|------------| | English | `en` | ✅ Complete | Core team | | German | `de` | 🚧 In Progress | Core team | +| Spanish | `es` | ✅ Complete | Community | +| Chinese | `zh` | ✅ Complete | Community | | Vietnamese | `vi` | ✅ Complete | Community | | Your Language | `??` | 🚧 In Progress | You? | --- -**Last Updated**: December 2025 +**Last Updated**: January 2026 diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 000000000..a5028553d --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,323 @@ +{ + "nav": { + "home": "Inicio", + "about": "Acerca de", + "contact": "Contacto", + "licensing": "Licencias", + "allTools": "Todas las Herramientas", + "openMainMenu": "Abrir menú principal", + "language": "Idioma" + }, + "donation": { + "message": "¿Te encanta BentoPDF? ¡Ayúdanos a mantenerlo gratis y de código abierto!", + "button": "Donar" + }, + "hero": { + "title": "El", + "pdfToolkit": "Kit de Herramientas PDF", + "builtForPrivacy": "diseñado para la privacidad", + "noSignups": "Sin Registro", + "unlimitedUse": "Uso Ilimitado", + "worksOffline": "Funciona Sin Conexión", + "startUsing": "Comenzar a Usar Ahora" + }, + "usedBy": { + "title": "Usado por empresas y personas que trabajan en" + }, + "features": { + "title": "¿Por qué elegir", + "bentoPdf": "BentoPDF?", + "noSignup": { + "title": "Sin Registro", + "description": "Comienza al instante, sin cuentas ni correos electrónicos." + }, + "noUploads": { + "title": "Sin Cargas", + "description": "100% del lado del cliente, tus archivos nunca salen de tu dispositivo." + }, + "foreverFree": { + "title": "Gratis para Siempre", + "description": "Todas las herramientas, sin pruebas, sin restricciones de pago." + }, + "noLimits": { + "title": "Sin Límites", + "description": "Usa tanto como quieras, sin límites ocultos." + }, + "batchProcessing": { + "title": "Procesamiento por Lotes", + "description": "Maneja PDFs ilimitados de una sola vez." + }, + "lightningFast": { + "title": "Ultrarrápido", + "description": "Procesa PDFs al instante, sin esperas ni retrasos." + } + }, + "tools": { + "title": "Comienza con", + "toolsLabel": "Herramientas", + "subtitle": "Haz clic en una herramienta para abrir el cargador de archivos", + "searchPlaceholder": "Buscar una herramienta (ej., 'dividir', 'organizar'...)", + "backToTools": "Volver a Herramientas", + "firstLoadNotice": "La primera carga toma un momento mientras descargamos nuestro motor de conversión. Después de eso, todas las cargas serán instantáneas." + }, + "upload": { + "clickToSelect": "Haz clic para seleccionar un archivo", + "orDragAndDrop": "o arrastra y suelta", + "pdfOrImages": "PDFs o Imágenes", + "filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.", + "addMore": "Agregar Más Archivos", + "clearAll": "Limpiar Todo" + }, + "loader": { + "processing": "Procesando..." + }, + "alert": { + "title": "Alerta", + "ok": "OK" + }, + "preview": { + "title": "Vista Previa del Documento", + "downloadAsPdf": "Descargar como PDF", + "close": "Cerrar" + }, + "settings": { + "title": "Configuración", + "shortcuts": "Atajos", + "preferences": "Preferencias", + "displayPreferences": "Preferencias de Visualización", + "searchShortcuts": "Buscar atajos...", + "shortcutsInfo": "Mantén presionadas las teclas para establecer un atajo. Los cambios se guardan automáticamente.", + "shortcutsWarning": "⚠️ Evita los atajos comunes del navegador (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N, etc.) ya que pueden no funcionar de manera confiable.", + "import": "Importar", + "export": "Exportar", + "resetToDefaults": "Restaurar Valores Predeterminados", + "fullWidthMode": "Modo de Ancho Completo", + "fullWidthDescription": "Usa el ancho completo de la pantalla para todas las herramientas en lugar de un contenedor centrado", + "settingsAutoSaved": "La configuración se guarda automáticamente", + "clickToSet": "Haz clic para establecer", + "pressKeys": "Presiona teclas...", + "warnings": { + "alreadyInUse": "Atajo Ya en Uso", + "assignedTo": "ya está asignado a:", + "chooseDifferent": "Por favor elige un atajo diferente.", + "reserved": "Advertencia de Atajo Reservado", + "commonlyUsed": "se usa comúnmente para:", + "unreliable": "Este atajo puede no funcionar de manera confiable o puede entrar en conflicto con el comportamiento del navegador/sistema.", + "useAnyway": "¿Quieres usarlo de todos modos?", + "resetTitle": "Restablecer Atajos", + "resetMessage": "¿Estás seguro de que quieres restablecer todos los atajos a los valores predeterminados?

Esta acción no se puede deshacer.", + "importSuccessTitle": "Importación Exitosa", + "importSuccessMessage": "¡Atajos importados exitosamente!", + "importFailTitle": "Importación Fallida", + "importFailMessage": "Error al importar atajos. Formato de archivo inválido." + } + }, + "warning": { + "title": "Advertencia", + "cancel": "Cancelar", + "proceed": "Continuar" + }, + "compliance": { + "title": "Tus datos nunca salen de tu dispositivo", + "weKeep": "Mantenemos", + "yourInfoSafe": "tu información segura", + "byFollowingStandards": "siguiendo estándares de seguridad globales.", + "processingLocal": "Todo el procesamiento ocurre localmente en tu dispositivo.", + "gdpr": { + "title": "Cumplimiento GDPR", + "description": "Protege los datos personales y la privacidad de las personas dentro de la Unión Europea." + }, + "ccpa": { + "title": "Cumplimiento CCPA", + "description": "Otorga a los residentes de California derechos sobre cómo se recopila, usa y comparte su información personal." + }, + "hipaa": { + "title": "Cumplimiento HIPAA", + "description": "Establece salvaguardas para el manejo de información de salud sensible en el sistema de atención médica de Estados Unidos." + } + }, + "faq": { + "title": "Preguntas", + "questions": "Frecuentes", + "isFree": { + "question": "¿BentoPDF es realmente gratis?", + "answer": "Sí, absolutamente. Todas las herramientas en BentoPDF son 100% gratuitas, sin límites de archivos, sin registro y sin marcas de agua. Creemos que todos merecen acceso a herramientas PDF simples y potentes sin un muro de pago." + }, + "areFilesSecure": { + "question": "¿Mis archivos están seguros? ¿Dónde se procesan?", + "answer": "Tus archivos están lo más seguros posible porque nunca salen de tu computadora. Todo el procesamiento ocurre directamente en tu navegador web (del lado del cliente). Nunca cargamos tus archivos a un servidor, por lo que mantienes total privacidad y control sobre tus documentos." + }, + "platforms": { + "question": "¿Funciona en Mac, Windows y Móvil?", + "answer": "¡Sí! Dado que BentoPDF se ejecuta completamente en tu navegador, funciona en cualquier sistema operativo con un navegador web moderno, incluyendo Windows, macOS, Linux, iOS y Android." + }, + "gdprCompliant": { + "question": "¿BentoPDF cumple con GDPR?", + "answer": "Sí. BentoPDF cumple completamente con GDPR. Dado que todo el procesamiento de archivos ocurre localmente en tu navegador y nunca recopilamos ni transmitimos tus archivos a ningún servidor, no tenemos acceso a tus datos. Esto garantiza que siempre tengas el control de tus documentos." + }, + "dataStorage": { + "question": "¿Almacenan o rastrean alguno de mis archivos?", + "answer": "No. Nunca almacenamos, rastreamos ni registramos tus archivos. Todo lo que haces en BentoPDF ocurre en la memoria de tu navegador y desaparece una vez que cierras la página. No hay cargas, no hay registros de historial y no hay servidores involucrados." + }, + "different": { + "question": "¿Qué hace que BentoPDF sea diferente de otras herramientas PDF?", + "answer": "La mayoría de las herramientas PDF cargan tus archivos a un servidor para procesarlos. BentoPDF nunca hace eso. Utilizamos tecnología web moderna y segura para procesar tus archivos directamente en tu navegador. Esto significa un rendimiento más rápido, mayor privacidad y total tranquilidad." + }, + "browserBased": { + "question": "¿Cómo me mantiene seguro el procesamiento basado en navegador?", + "answer": "Al ejecutarse completamente dentro de tu navegador, BentoPDF garantiza que tus archivos nunca salgan de tu dispositivo. Esto elimina los riesgos de hackeos de servidores, violaciones de datos o accesos no autorizados. Tus archivos siguen siendo tuyos, siempre." + }, + "analytics": { + "question": "¿Usan cookies o análisis para rastrearme?", + "answer": "Nos preocupamos por tu privacidad. BentoPDF no rastrea información personal. Usamos Simple Analytics únicamente para ver recuentos de visitas anónimas. Esto significa que podemos saber cuántos usuarios visitan nuestro sitio, pero nunca sabemos quién eres. Simple Analytics cumple completamente con GDPR y respeta tu privacidad." + } + }, + "testimonials": { + "title": "Lo que Nuestros", + "users": "Usuarios", + "say": "Dicen" + }, + "support": { + "title": "¿Te Gusta Mi Trabajo?", + "description": "BentoPDF es un proyecto de pasión, creado para proporcionar un kit de herramientas PDF gratuito, privado y potente para todos. Si te resulta útil, considera apoyar su desarrollo. ¡Cada café ayuda!", + "buyMeCoffee": "Cómprame un Café" + }, + "footer": { + "copyright": "© 2025 BentoPDF. Todos los derechos reservados.", + "version": "Versión", + "company": "Empresa", + "aboutUs": "Acerca de Nosotros", + "faqLink": "Preguntas Frecuentes", + "contactUs": "Contáctanos", + "legal": "Legal", + "termsAndConditions": "Términos y Condiciones", + "privacyPolicy": "Política de Privacidad", + "followUs": "Síguenos" + }, + "merge": { + "title": "Fusionar PDFs", + "description": "Combina archivos completos o selecciona páginas específicas para fusionar en un nuevo documento.", + "fileMode": "Modo Archivo", + "pageMode": "Modo Página", + "howItWorks": "Cómo funciona:", + "fileModeInstructions": [ + "Haz clic y arrastra el ícono para cambiar el orden de los archivos.", + "En el cuadro \"Páginas\" para cada archivo, puedes especificar rangos (ej., \"1-3, 5\") para fusionar solo esas páginas.", + "Deja el cuadro \"Páginas\" en blanco para incluir todas las páginas de ese archivo." + ], + "pageModeInstructions": [ + "Todas las páginas de tus PDFs cargados se muestran a continuación.", + "Simplemente arrastra y suelta las miniaturas de páginas individuales para crear el orden exacto que deseas para tu nuevo archivo." + ], + "mergePdfs": "Fusionar PDFs" + }, + "common": { + "page": "Página", + "pages": "Páginas", + "of": "de", + "download": "Descargar", + "cancel": "Cancelar", + "save": "Guardar", + "delete": "Eliminar", + "edit": "Editar", + "add": "Agregar", + "remove": "Remover", + "loading": "Cargando...", + "error": "Error", + "success": "Éxito", + "file": "Archivo", + "files": "Archivos" + }, + "about": { + "hero": { + "title": "Creemos que las herramientas PDF deben ser", + "subtitle": "rápidas, privadas y gratuitas.", + "noCompromises": "Sin compromisos." + }, + "mission": { + "title": "Nuestra Misión", + "description": "Proporcionar la caja de herramientas PDF más completa que respete tu privacidad y nunca pida pago. Creemos que las herramientas de documentos esenciales deben ser accesibles para todos, en todas partes, sin barreras." + }, + "philosophy": { + "label": "Nuestra Filosofía Central", + "title": "Privacidad Primero. Siempre.", + "description": "En una era donde los datos son una mercancía, adoptamos un enfoque diferente. Todo el procesamiento de las herramientas de Bentopdf ocurre localmente en tu navegador. Esto significa que tus archivos nunca tocan nuestros servidores, nunca vemos tus documentos y no rastreamos lo que haces. Tus documentos permanecen completa e inequívocamente privados. No es solo una característica; es nuestra base." + }, + "whyBentopdf": { + "title": "Por qué", + "speed": { + "title": "Diseñado para la Velocidad", + "description": "Sin esperar cargas o descargas a un servidor. Al procesar archivos directamente en tu navegador usando tecnologías web modernas como WebAssembly, ofrecemos una velocidad incomparable para todas nuestras herramientas." + }, + "free": { + "title": "Completamente Gratis", + "description": "Sin pruebas, sin suscripciones, sin tarifas ocultas y sin funciones \"premium\" retenidas como rehenes. Creemos que las herramientas PDF potentes deben ser una utilidad pública, no un centro de ganancias." + }, + "noAccount": { + "title": "No Requiere Cuenta", + "description": "Comienza a usar cualquier herramienta de inmediato. No necesitamos tu correo electrónico, una contraseña o cualquier información personal. Tu flujo de trabajo debe ser sin fricciones y anónimo." + }, + "openSource": { + "title": "Espíritu de Código Abierto", + "description": "Construido con transparencia en mente. Aprovechamos increíbles bibliotecas de código abierto como PDF-lib y PDF.js, y creemos en el esfuerzo impulsado por la comunidad para hacer que las herramientas potentes sean accesibles para todos." + } + }, + "cta": { + "title": "¿Listo para comenzar?", + "description": "Únete a miles de usuarios que confían en Bentopdf para sus necesidades diarias de documentos. Experimenta la diferencia que la privacidad y el rendimiento pueden hacer.", + "button": "Explorar Todas las Herramientas" + } + }, + "contact": { + "title": "Ponte en Contacto", + "subtitle": "Nos encantaría saber de ti. Ya sea que tengas una pregunta, comentario o solicitud de función, no dudes en comunicarte.", + "email": "Puedes contactarnos directamente por correo electrónico en:" + }, + "licensing": { + "title": "Licencias para", + "subtitle": "Elige la licencia que se ajuste a tus necesidades." + }, + "multiTool": { + "uploadPdfs": "Cargar PDFs", + "upload": "Cargar", + "addBlankPage": "Agregar Página en Blanco", + "edit": "Editar:", + "undo": "Deshacer", + "redo": "Rehacer", + "reset": "Restablecer", + "selection": "Selección:", + "selectAll": "Seleccionar Todo", + "deselectAll": "Deseleccionar Todo", + "rotate": "Rotar:", + "rotateLeft": "Izquierda", + "rotateRight": "Derecha", + "transform": "Transformar:", + "duplicate": "Duplicar", + "split": "Dividir", + "clear": "Limpiar:", + "delete": "Eliminar", + "download": "Descargar:", + "downloadSelected": "Descargar Seleccionados", + "exportPdf": "Exportar PDF", + "uploadPdfFiles": "Seleccionar Archivos PDF", + "dragAndDrop": "Arrastra y suelta archivos PDF aquí, o haz clic para seleccionar", + "selectFiles": "Seleccionar Archivos", + "renderingPages": "Renderizando páginas...", + "actions": { + "duplicatePage": "Duplicar esta página", + "deletePage": "Eliminar esta página", + "insertPdf": "Insertar PDF después de esta página", + "toggleSplit": "Alternar división después de esta página" + }, + "pleaseWait": "Por Favor Espera", + "pagesRendering": "Las páginas aún se están renderizando. Por favor espera...", + "noPagesSelected": "No Se Seleccionaron Páginas", + "selectOnePage": "Por favor selecciona al menos una página para descargar.", + "noPages": "Sin Páginas", + "noPagesToExport": "No hay páginas para exportar.", + "renderingTitle": "Renderizando vistas previas de páginas", + "errorRendering": "Error al renderizar miniaturas de páginas", + "error": "Error", + "failedToLoad": "Error al cargar" + } +} diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json new file mode 100644 index 000000000..7839a2ea8 --- /dev/null +++ b/public/locales/es/tools.json @@ -0,0 +1,492 @@ +{ + "categories": { + "popularTools": "Herramientas Populares", + "editAnnotate": "Editar y Anotar", + "convertToPdf": "Convertir a PDF", + "convertFromPdf": "Convertir desde PDF", + "organizeManage": "Organizar y Gestionar", + "optimizeRepair": "Optimizar y Reparar", + "securePdf": "Asegurar PDF" + }, + "pdfMultiTool": { + "name": "Multiherramienta PDF", + "subtitle": "Fusionar, Dividir, Organizar, Eliminar, Rotar, Agregar Páginas en Blanco, Extraer y Duplicar en una interfaz unificada." + }, + "mergePdf": { + "name": "Fusionar PDF", + "subtitle": "Combina múltiples PDFs en un solo archivo. Preserva Marcadores." + }, + "splitPdf": { + "name": "Dividir PDF", + "subtitle": "Extrae un rango de páginas en un nuevo PDF." + }, + "compressPdf": { + "name": "Comprimir PDF", + "subtitle": "Reduce el tamaño de archivo de tu PDF.", + "algorithmLabel": "Algoritmo de Compresión", + "condense": "Condensar (Recomendado)", + "photon": "Photon (Para PDFs con Muchas Fotos)", + "condenseInfo": "Condensar usa compresión avanzada: elimina peso muerto, optimiza imágenes, reduce fuentes. Mejor para la mayoría de PDFs.", + "photonInfo": "Photon convierte páginas en imágenes. Úsalo para PDFs con muchas fotos/escaneados.", + "photonWarning": "Advertencia: El texto dejará de ser seleccionable y los enlaces dejarán de funcionar.", + "levelLabel": "Nivel de Compresión", + "light": "Ligero (Preservar Calidad)", + "balanced": "Equilibrado (Recomendado)", + "aggressive": "Agresivo (Archivos Más Pequeños)", + "extreme": "Extremo (Compresión Máxima)", + "grayscale": "Convertir a Escala de Grises", + "grayscaleHint": "Reduce el tamaño del archivo eliminando información de color", + "customSettings": "Configuración Personalizada", + "customSettingsHint": "Ajusta los parámetros de compresión:", + "outputQuality": "Calidad de Salida", + "resizeImagesTo": "Redimensionar Imágenes a", + "onlyProcessAbove": "Solo Procesar Arriba de", + "removeMetadata": "Eliminar metadatos", + "subsetFonts": "Reducir fuentes (eliminar glifos no usados)", + "removeThumbnails": "Eliminar miniaturas incrustadas", + "compressButton": "Comprimir PDF" + }, + "pdfEditor": { + "name": "Editor PDF", + "subtitle": "Anotar, resaltar, redactar, comentar, agregar formas/imágenes, buscar y ver PDFs." + }, + "jpgToPdf": { + "name": "JPG a PDF", + "subtitle": "Crea un PDF desde imágenes JPG, JPEG y JPEG2000 (JP2/JPX)." + }, + "signPdf": { + "name": "Firmar PDF", + "subtitle": "Dibuja, escribe o carga tu firma." + }, + "cropPdf": { + "name": "Recortar PDF", + "subtitle": "Recorta los márgenes de cada página en tu PDF." + }, + "extractPages": { + "name": "Extraer Páginas", + "subtitle": "Guarda una selección de páginas como nuevos archivos." + }, + "duplicateOrganize": { + "name": "Duplicar y Organizar", + "subtitle": "Duplica, reordena y elimina páginas." + }, + "deletePages": { + "name": "Eliminar Páginas", + "subtitle": "Elimina páginas específicas de tu documento." + }, + "editBookmarks": { + "name": "Editar Marcadores", + "subtitle": "Agrega, edita, importa, elimina y extrae marcadores PDF." + }, + "tableOfContents": { + "name": "Tabla de Contenidos", + "subtitle": "Genera una página de tabla de contenidos desde los marcadores PDF." + }, + "pageNumbers": { + "name": "Números de Página", + "subtitle": "Inserta números de página en tu documento." + }, + "addWatermark": { + "name": "Agregar Marca de Agua", + "subtitle": "Estampa texto o una imagen sobre tus páginas PDF." + }, + "headerFooter": { + "name": "Encabezado y Pie de Página", + "subtitle": "Agrega texto en la parte superior e inferior de las páginas." + }, + "invertColors": { + "name": "Invertir Colores", + "subtitle": "Crea una versión en \"modo oscuro\" de tu PDF." + }, + "backgroundColor": { + "name": "Color de Fondo", + "subtitle": "Cambia el color de fondo de tu PDF." + }, + "changeTextColor": { + "name": "Cambiar Color de Texto", + "subtitle": "Cambia el color del texto en tu PDF." + }, + "addStamps": { + "name": "Agregar Sellos", + "subtitle": "Agrega sellos de imagen a tu PDF usando la barra de herramientas de anotación.", + "usernameLabel": "Nombre de Usuario del Sello", + "usernamePlaceholder": "Ingresa tu nombre (para sellos)", + "usernameHint": "Este nombre aparecerá en los sellos que crees." + }, + "removeAnnotations": { + "name": "Eliminar Anotaciones", + "subtitle": "Elimina comentarios, resaltados y enlaces." + }, + "pdfFormFiller": { + "name": "Rellenar Formularios PDF", + "subtitle": "Rellena formularios directamente en el navegador. También soporta formularios XFA." + }, + "createPdfForm": { + "name": "Crear Formulario PDF", + "subtitle": "Crea formularios PDF rellenables con campos de texto arrastrables." + }, + "removeBlankPages": { + "name": "Eliminar Páginas en Blanco", + "subtitle": "Detecta y elimina automáticamente páginas en blanco." + }, + "imageToPdf": { + "name": "Imágenes a PDF", + "subtitle": "Convierte JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP a PDF." + }, + "pngToPdf": { + "name": "PNG a PDF", + "subtitle": "Crea un PDF desde una o más imágenes PNG." + }, + "webpToPdf": { + "name": "WebP a PDF", + "subtitle": "Crea un PDF desde una o más imágenes WebP." + }, + "svgToPdf": { + "name": "SVG a PDF", + "subtitle": "Crea un PDF desde una o más imágenes SVG." + }, + "bmpToPdf": { + "name": "BMP a PDF", + "subtitle": "Crea un PDF desde una o más imágenes BMP." + }, + "heicToPdf": { + "name": "HEIC a PDF", + "subtitle": "Crea un PDF desde una o más imágenes HEIC." + }, + "tiffToPdf": { + "name": "TIFF a PDF", + "subtitle": "Crea un PDF desde una o más imágenes TIFF." + }, + "textToPdf": { + "name": "Texto a PDF", + "subtitle": "Convierte un archivo de texto plano en un PDF." + }, + "jsonToPdf": { + "name": "JSON a PDF", + "subtitle": "Convierte archivos JSON a formato PDF." + }, + "pdfToJpg": { + "name": "PDF a JPG", + "subtitle": "Convierte cada página PDF en una imagen JPG." + }, + "pdfToPng": { + "name": "PDF a PNG", + "subtitle": "Convierte cada página PDF en una imagen PNG." + }, + "pdfToWebp": { + "name": "PDF a WebP", + "subtitle": "Convierte cada página PDF en una imagen WebP." + }, + "pdfToBmp": { + "name": "PDF a BMP", + "subtitle": "Convierte cada página PDF en una imagen BMP." + }, + "pdfToTiff": { + "name": "PDF a TIFF", + "subtitle": "Convierte cada página PDF en una imagen TIFF." + }, + "pdfToGreyscale": { + "name": "PDF a Escala de Grises", + "subtitle": "Convierte todos los colores a blanco y negro." + }, + "pdfToJson": { + "name": "PDF a JSON", + "subtitle": "Convierte archivos PDF a formato JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Hace que un PDF sea buscable y copiable." + }, + "alternateMix": { + "name": "Alternar y Mezclar Páginas", + "subtitle": "Fusiona PDFs alternando páginas de cada PDF. Preserva Marcadores." + }, + "addAttachments": { + "name": "Agregar Adjuntos", + "subtitle": "Incrusta uno o más archivos en tu PDF." + }, + "extractAttachments": { + "name": "Extraer Adjuntos", + "subtitle": "Extrae todos los archivos incrustados de PDF(s) como un ZIP." + }, + "editAttachments": { + "name": "Editar Adjuntos", + "subtitle": "Ve o elimina adjuntos en tu PDF." + }, + "dividePages": { + "name": "Dividir Páginas", + "subtitle": "Divide páginas horizontal o verticalmente." + }, + "addBlankPage": { + "name": "Agregar Página en Blanco", + "subtitle": "Inserta una página vacía en cualquier lugar de tu PDF." + }, + "reversePages": { + "name": "Invertir Páginas", + "subtitle": "Invierte el orden de todas las páginas en tu documento." + }, + "rotatePdf": { + "name": "Rotar PDF", + "subtitle": "Gira páginas en incrementos de 90 grados." + }, + "rotateCustom": { + "name": "Rotar por Grados Personalizados", + "subtitle": "Rota páginas por cualquier ángulo personalizado." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Organiza múltiples páginas en una sola hoja." + }, + "combineToSinglePage": { + "name": "Combinar en Una Sola Página", + "subtitle": "Une todas las páginas en un desplazamiento continuo." + }, + "viewMetadata": { + "name": "Ver Metadatos", + "subtitle": "Inspecciona las propiedades ocultas de tu PDF." + }, + "editMetadata": { + "name": "Editar Metadatos", + "subtitle": "Cambia el autor, título y otras propiedades." + }, + "pdfsToZip": { + "name": "PDFs a ZIP", + "subtitle": "Empaqueta múltiples archivos PDF en un archivo ZIP." + }, + "comparePdfs": { + "name": "Comparar PDFs", + "subtitle": "Compara dos PDFs lado a lado." + }, + "posterizePdf": { + "name": "Posterizar PDF", + "subtitle": "Divide una página grande en múltiples páginas más pequeñas." + }, + "fixPageSize": { + "name": "Fijar Tamaño de Página", + "subtitle": "Estandariza todas las páginas a un tamaño uniforme." + }, + "linearizePdf": { + "name": "Linealizar PDF", + "subtitle": "Optimiza el PDF para visualización web rápida." + }, + "pageDimensions": { + "name": "Dimensiones de Página", + "subtitle": "Analiza el tamaño, orientación y unidades de página." + }, + "removeRestrictions": { + "name": "Eliminar Restricciones", + "subtitle": "Elimina la protección por contraseña y las restricciones de seguridad asociadas con archivos PDF firmados digitalmente." + }, + "repairPdf": { + "name": "Reparar PDF", + "subtitle": "Recupera datos de archivos PDF corruptos o dañados." + }, + "encryptPdf": { + "name": "Cifrar PDF", + "subtitle": "Bloquea tu PDF agregando una contraseña." + }, + "sanitizePdf": { + "name": "Sanear PDF", + "subtitle": "Elimina metadatos, anotaciones, scripts y más." + }, + "decryptPdf": { + "name": "Descifrar PDF", + "subtitle": "Desbloquea PDF eliminando la protección por contraseña." + }, + "flattenPdf": { + "name": "Aplanar PDF", + "subtitle": "Hace que los campos de formulario y las anotaciones no sean editables." + }, + "removeMetadata": { + "name": "Eliminar Metadatos", + "subtitle": "Elimina datos ocultos de tu PDF." + }, + "changePermissions": { + "name": "Cambiar Permisos", + "subtitle": "Establece o cambia los permisos de usuario en un PDF." + }, + "odtToPdf": { + "name": "ODT a PDF", + "subtitle": "Convierte archivos OpenDocument Text a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos ODT", + "convertButton": "Convertir a PDF" + }, + "csvToPdf": { + "name": "CSV a PDF", + "subtitle": "Convierte archivos de hoja de cálculo CSV a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos CSV", + "convertButton": "Convertir a PDF" + }, + "rtfToPdf": { + "name": "RTF a PDF", + "subtitle": "Convierte documentos Rich Text Format a PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos RTF", + "convertButton": "Convertir a PDF" + }, + "wordToPdf": { + "name": "Word a PDF", + "subtitle": "Convierte documentos Word (DOCX, DOC, ODT, RTF) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos DOCX, DOC, ODT, RTF", + "convertButton": "Convertir a PDF" + }, + "excelToPdf": { + "name": "Excel a PDF", + "subtitle": "Convierte hojas de cálculo Excel (XLSX, XLS, ODS, CSV) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos XLSX, XLS, ODS, CSV", + "convertButton": "Convertir a PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint a PDF", + "subtitle": "Convierte presentaciones PowerPoint (PPTX, PPT, ODP) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos PPTX, PPT, ODP", + "convertButton": "Convertir a PDF" + }, + "markdownToPdf": { + "name": "Markdown a PDF", + "subtitle": "Escribe o pega Markdown y expórtalo como un PDF bellamente formateado.", + "paneMarkdown": "Markdown", + "panePreview": "Vista Previa", + "btnUpload": "Cargar", + "btnSyncScroll": "Sincronizar Desplazamiento", + "btnSettings": "Configuración", + "btnExportPdf": "Exportar PDF", + "settingsTitle": "Configuración de Markdown", + "settingsPreset": "Predefinido", + "presetDefault": "Predeterminado (similar a GFM)", + "presetCommonmark": "CommonMark (estricto)", + "presetZero": "Mínimo (sin funciones)", + "settingsOptions": "Opciones de Markdown", + "optAllowHtml": "Permitir etiquetas HTML", + "optBreaks": "Convertir saltos de línea a
", + "optLinkify": "Auto-convertir URLs a enlaces", + "optTypographer": "Tipógrafo (comillas inteligentes, etc.)" + }, + "pdfBooklet": { + "name": "Folleto PDF", + "subtitle": "Reorganiza páginas para impresión de folleto a doble cara. Dobla y engrapa para crear un folleto.", + "howItWorks": "Cómo funciona:", + "step1": "Carga un archivo PDF.", + "step2": "Las páginas se reorganizarán en orden de folleto.", + "step3": "Imprime a doble cara, voltea por el borde corto, dobla y engrapa.", + "paperSize": "Tamaño de Papel", + "orientation": "Orientación", + "portrait": "Vertical", + "landscape": "Horizontal", + "pagesPerSheet": "Páginas por Hoja", + "createBooklet": "Crear Folleto", + "processing": "Procesando...", + "pageCount": "El recuento de páginas se rellenará a múltiplo de 4 si es necesario." + }, + "xpsToPdf": { + "name": "XPS a PDF", + "subtitle": "Convierte documentos XPS/OXPS a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos XPS, OXPS", + "convertButton": "Convertir a PDF" + }, + "mobiToPdf": { + "name": "MOBI a PDF", + "subtitle": "Convierte e-books MOBI a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos MOBI", + "convertButton": "Convertir a PDF" + }, + "epubToPdf": { + "name": "EPUB a PDF", + "subtitle": "Convierte e-books EPUB a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos EPUB", + "convertButton": "Convertir a PDF" + }, + "fb2ToPdf": { + "name": "FB2 a PDF", + "subtitle": "Convierte e-books FictionBook (FB2) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos FB2", + "convertButton": "Convertir a PDF" + }, + "cbzToPdf": { + "name": "CBZ a PDF", + "subtitle": "Convierte archivos de cómics (CBZ/CBR) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos CBZ, CBR", + "convertButton": "Convertir a PDF" + }, + "wpdToPdf": { + "name": "WPD a PDF", + "subtitle": "Convierte documentos WordPerfect (WPD) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos WPD", + "convertButton": "Convertir a PDF" + }, + "wpsToPdf": { + "name": "WPS a PDF", + "subtitle": "Convierte documentos WPS Office a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos WPS", + "convertButton": "Convertir a PDF" + }, + "xmlToPdf": { + "name": "XML a PDF", + "subtitle": "Convierte documentos XML a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos XML", + "convertButton": "Convertir a PDF" + }, + "pagesToPdf": { + "name": "Pages a PDF", + "subtitle": "Convierte documentos Apple Pages a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos Pages", + "convertButton": "Convertir a PDF" + }, + "odgToPdf": { + "name": "ODG a PDF", + "subtitle": "Convierte archivos OpenDocument Graphics (ODG) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos ODG", + "convertButton": "Convertir a PDF" + }, + "odsToPdf": { + "name": "ODS a PDF", + "subtitle": "Convierte archivos OpenDocument Spreadsheet (ODS) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos ODS", + "convertButton": "Convertir a PDF" + }, + "odpToPdf": { + "name": "ODP a PDF", + "subtitle": "Convierte archivos OpenDocument Presentation (ODP) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos ODP", + "convertButton": "Convertir a PDF" + }, + "pubToPdf": { + "name": "PUB a PDF", + "subtitle": "Convierte archivos Microsoft Publisher (PUB) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos PUB", + "convertButton": "Convertir a PDF" + }, + "vsdToPdf": { + "name": "VSD a PDF", + "subtitle": "Convierte archivos Microsoft Visio (VSD, VSDX) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos VSD, VSDX", + "convertButton": "Convertir a PDF" + }, + "psdToPdf": { + "name": "PSD a PDF", + "subtitle": "Convierte archivos Adobe Photoshop (PSD) a formato PDF. Soporta múltiples archivos.", + "acceptedFormats": "Archivos PSD", + "convertButton": "Convertir a PDF" + }, + "pdfToSvg": { + "name": "PDF a SVG", + "subtitle": "Convierte cada página de un archivo PDF en un gráfico vectorial escalable (SVG) para calidad perfecta a cualquier tamaño." + }, + "extractTables": { + "name": "Extraer Tablas de PDF", + "subtitle": "Extrae tablas de archivos PDF y exporta como CSV, JSON o Markdown." + }, + "pdfToCsv": { + "name": "PDF a CSV", + "subtitle": "Extrae tablas de PDF y convierte a formato CSV." + }, + "pdfToExcel": { + "name": "PDF a Excel", + "subtitle": "Extrae tablas de PDF y convierte a formato Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF a Texto", + "subtitle": "Extrae texto de archivos PDF y guarda como texto plano (.txt). Soporta múltiples archivos.", + "note": "Esta herramienta funciona SOLO con PDFs creados digitalmente. Para documentos escaneados o PDFs basados en imágenes, usa nuestra herramienta OCR PDF en su lugar.", + "convertButton": "Extraer Texto" + } +} diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index 62eca2bbc..2793696ef 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -3,19 +3,20 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // Supported languages -export const supportedLanguages = ['en', 'de', 'zh', 'vi'] as const; +export const supportedLanguages = ['en', 'de', 'es', 'zh', 'vi'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { en: 'English', de: 'Deutsch', + es: 'Español', zh: '中文', vi: 'Tiếng Việt', }; export const getLanguageFromUrl = (): SupportedLanguage => { const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi)(?:\/|$)/); + const langMatch = path.match(/^\/(en|de|es|zh|vi)(?:\/|$)/); if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) { return langMatch[1] as SupportedLanguage; } diff --git a/vite.config.ts b/vite.config.ts index ce8a7078c..4029d5c2a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ function pagesRewritePlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url?.split('?')[0] || ''; - const langMatch = url.match(/^\/(en|de|zh|vi)(\/.*)?$/); + const langMatch = url.match(/^\/(en|de|es|zh|vi)(\/.*)?$/); if (langMatch) { const lang = langMatch[1]; const restOfPath = langMatch[2] || '/'; From 528b14f60449eb9e22b7a582844a6ac7d6fc8524 Mon Sep 17 00:00:00 2001 From: Raul Gonzalez Date: Fri, 2 Jan 2026 07:27:28 -0600 Subject: [PATCH 04/39] Sign ICLA --- ICLA.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ICLA.md b/ICLA.md index c80debbf4..f7e620756 100644 --- a/ICLA.md +++ b/ICLA.md @@ -85,15 +85,15 @@ This Agreement shall be governed by and construed in accordance with the laws of By submitting a pull request or other Contribution to the Project, and by typing your name and date below (or by signing electronically via CLA Assistant), you agree to the terms of this Individual Contributor License Agreement. -**Full Legal Name:** Stephan Paternotte +**Full Legal Name:** Raul Gonzalez -**GitHub Username:** Stephan-P +**GitHub Username:** raulgcode -**Email Address:** stephan@paternottes.net +**Email Address:** armandourbina@gmail.com -**Date:** 20-12-2025 +**Date:** 02-01-2026 -**Signature:** ___________________________ +**Signature:** Raul Gonzalez --- From 2b0ddfebfeeeace76ef1e69b229c35f80aa59ee2 Mon Sep 17 00:00:00 2001 From: Raul Gonzalez Date: Fri, 2 Jan 2026 07:38:29 -0600 Subject: [PATCH 05/39] fix: add Spanish language support in URL rewrite rules --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index 56fab7acb..8de1d2bdc 100644 --- a/nginx.conf +++ b/nginx.conf @@ -26,7 +26,7 @@ http { root /usr/share/nginx/html; index index.html; - rewrite ^/(en|de|zh|vi)/(.*)$ /$2 last; + rewrite ^/(en|de|es|zh|vi)/(.*)$ /$2 last; location ~* \.html$ { expires 1h; From f39a29e2cc0ac004415afb46835bcd5dc2a84471 Mon Sep 17 00:00:00 2001 From: Raul Gonzalez Date: Fri, 2 Jan 2026 07:40:12 -0600 Subject: [PATCH 06/39] fix: Rollback changes in ICLA file --- ICLA.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ICLA.md b/ICLA.md index f7e620756..c80debbf4 100644 --- a/ICLA.md +++ b/ICLA.md @@ -85,15 +85,15 @@ This Agreement shall be governed by and construed in accordance with the laws of By submitting a pull request or other Contribution to the Project, and by typing your name and date below (or by signing electronically via CLA Assistant), you agree to the terms of this Individual Contributor License Agreement. -**Full Legal Name:** Raul Gonzalez +**Full Legal Name:** Stephan Paternotte -**GitHub Username:** raulgcode +**GitHub Username:** Stephan-P -**Email Address:** armandourbina@gmail.com +**Email Address:** stephan@paternottes.net -**Date:** 02-01-2026 +**Date:** 20-12-2025 -**Signature:** Raul Gonzalez +**Signature:** ___________________________ --- From 0491c1118ea9fba3373601e95c3682ecf8abe697 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:21:15 +0000 Subject: [PATCH 07/39] build(deps): bump jspdf from 3.0.4 to 4.0.0 Bumps [jspdf](https://github.com/parallax/jsPDF) from 3.0.4 to 4.0.0. - [Release notes](https://github.com/parallax/jsPDF/releases) - [Changelog](https://github.com/parallax/jsPDF/blob/master/RELEASE.md) - [Commits](https://github.com/parallax/jsPDF/compare/v3.0.4...v4.0.0) --- updated-dependencies: - dependency-name: jspdf dependency-version: 4.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 119 +++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 76 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85740f5b8..ca83c111b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "i18next": "^25.7.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "jspdf": "^3.0.3", + "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.2", "jszip": "^3.10.1", "lucide": "^0.546.0", @@ -263,7 +263,6 @@ "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", @@ -508,7 +507,6 @@ "resolved": "https://registry.npmjs.org/@bentopdf/gs-wasm/-/gs-wasm-0.1.0.tgz", "integrity": "sha512-C71zxZW4R7Oa6fdya5leTh2VOZOxqH8IQlveh13OeuwZ2ulrovSi9629xTzAiIeeVKvDZma1Klxy4MuK65xe9w==", "license": "AGPL-3.0", - "peer": true, "dependencies": { "@types/emscripten": "^1.39.10" } @@ -667,7 +665,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -711,7 +708,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -772,7 +768,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz", "integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/engines": "1.5.0", "@embedpdf/models": "1.5.0" @@ -923,7 +918,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz", "integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -940,7 +934,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz", "integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -958,7 +951,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz", "integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1031,7 +1023,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", "integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1066,7 +1057,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz", "integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1103,7 +1093,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz", "integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1194,7 +1183,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz", "integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -2880,6 +2868,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", "license": "MIT", + "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -3110,6 +3099,60 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", @@ -3521,7 +3564,6 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -3676,7 +3718,6 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -4035,7 +4076,6 @@ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", @@ -4350,7 +4390,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4401,7 +4440,6 @@ "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.12.2", "@algolia/client-abtesting": "5.46.2", @@ -4616,6 +4654,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -5291,6 +5330,7 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -5617,7 +5657,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -6027,7 +6066,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6263,7 +6301,8 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/devlop": { "version": "1.1.0", @@ -6564,7 +6603,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6669,7 +6707,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/espree": { "version": "10.4.0", @@ -6707,6 +6746,7 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz", "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -6927,7 +6967,6 @@ "integrity": "sha512-Pkp8m55GjxBLnhBoT6OXdMvfRr4TjMAKLvFM566zlIryq5plbhaTmLAJWTGR0EkRwLjEte1lCOG9MxF1ipJrOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -7767,6 +7806,7 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -7969,7 +8009,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -8039,11 +8078,10 @@ } }, "node_modules/jspdf": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", - "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -8626,7 +8664,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -8744,6 +8783,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -8833,7 +8873,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -9939,7 +9978,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.1.tgz", "integrity": "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -10416,7 +10454,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10536,6 +10573,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -10788,8 +10826,7 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", @@ -11172,6 +11209,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -11235,7 +11273,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -11427,7 +11464,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11628,7 +11664,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11905,7 +11940,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12070,7 +12104,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12570,7 +12603,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -12631,7 +12663,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -12780,7 +12811,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -13108,7 +13138,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/zip-stream": { "version": "6.0.1", diff --git a/package.json b/package.json index 3b9ae0a9c..edb68b317 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "i18next": "^25.7.2", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "jspdf": "^3.0.3", + "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.2", "jszip": "^3.10.1", "lucide": "^0.546.0", From e399315798dc5971f31a5cea51d09cc7037d8718 Mon Sep 17 00:00:00 2001 From: Hanif Naufal Date: Wed, 7 Jan 2026 22:46:54 +0700 Subject: [PATCH 08/39] feat: add Indonesian language support to translation guide --- TRANSLATION.md | 55 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 00354a5da..6ad2587a9 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -22,11 +22,14 @@ BentoPDF uses **i18next** for internationalization (i18n). Currently supported l - **English** (`en`) - Default - **German** (`de`) - **Vietnamese** (`vi`) +- **Indonesian** (`id`) The app automatically detects the language from the URL path: + - `/en/` → English - `/de/` → German - `/vi/` → Vietnamese +- `/id/` → Indonesian --- @@ -86,11 +89,13 @@ Open `public/locales/fr/common.json` and translate all the values: ⚠️ **Important**: Only translate the **values**, NOT the keys! ✅ **Correct:** + ```json "home": "Accueil" ``` ❌ **Wrong:** + ```json "accueil": "Accueil" ``` @@ -106,9 +111,9 @@ export type SupportedLanguage = (typeof supportedLanguages)[number]; // Add French display name export const languageNames: Record = { - en: 'English', - de: 'Deutsch', - fr: 'Français', // ← Add this + en: 'English', + de: 'Deutsch', + fr: 'Français', // ← Add this }; ``` @@ -207,6 +212,7 @@ Tool names and descriptions are defined in `src/js/config/tools.ts` and use a sp ``` In translations: + ```json { "tools": { @@ -234,14 +240,15 @@ console.log(message); // "Error" or "Fehler" depending on language For input placeholders: ```html - ``` In `common.json`: + ```json { "tools": { @@ -257,6 +264,7 @@ In `common.json`: ### Manual Testing 1. **Start development server:** + ```bash npm run dev ``` @@ -265,6 +273,7 @@ In `common.json`: - English: `http://localhost:5173/en/` - German: `http://localhost:5173/de/` - Vietnamese: `http://localhost:5173/vi/` + - Indonesian: `http://localhost:5173/id/` - Your new language: `http://localhost:5173/fr/` 3. **Check these pages:** @@ -289,11 +298,12 @@ Check for missing translations: node scripts/check-translations.js ``` -*(If this script doesn't exist, you may need to create it or manually compare JSON files)* +_(If this script doesn't exist, you may need to create it or manually compare JSON files)_ ### Browser Testing Test in different browsers: + - Chrome/Edge - Firefox - Safari @@ -307,11 +317,13 @@ Test in different browsers: BentoPDF is **friendly, clear, and professional**. Match this tone in your translations. ✅ **Good:** + ```json "hero.title": "Ihr kostenloses und sicheres PDF-Toolkit" ``` ❌ **Too formal:** + ```json "hero.title": "Ihr gebührenfreies und gesichertes Werkzeug für PDF-Dokumente" ``` @@ -339,6 +351,7 @@ When translating, **keep the HTML tags intact**: If your language has complex plural rules or gender distinctions, consult the [i18next pluralization guide](https://www.i18next.com/translation-function/plurals). Example: + ```json { "pages": "page", @@ -349,6 +362,7 @@ Example: ### 4. Don't Translate Brand Names or Legal Terms Keep these as-is: + - BentoPDF - PDF - GitHub @@ -361,6 +375,7 @@ Keep these as-is: ### 5. Technical Terms For technical terms, use commonly accepted translations in your language: + - "Merge" → "Fusionner" (French), "Zusammenführen" (German) - "Split" → "Diviser" (French), "Teilen" (German) - "Compress" → "Compresser" (French), "Komprimieren" (German) @@ -380,6 +395,7 @@ If a translation is much longer, test it visually to ensure it doesn't break the ### Issue: Translations Not Showing Up **Solution:** + 1. Clear your browser cache 2. Hard refresh (Ctrl+F5 or Cmd+Shift+R) 3. Check browser console for errors @@ -388,22 +404,26 @@ If a translation is much longer, test it visually to ensure it doesn't break the ### Issue: Some Text Still in English **Possible causes:** + 1. Missing translation key in your language file 2. Missing `data-i18n` attribute in HTML 3. Hardcoded text in JavaScript **Solution:** + - Compare your language file with `en/common.json` to find missing keys - Search the codebase for hardcoded strings ### Issue: JSON Syntax Error **Symptoms:** + ``` SyntaxError: Unexpected token } in JSON at position 1234 ``` **Solution:** + - Use a JSON validator: https://jsonlint.com/ - Common mistakes: - Trailing comma after last item @@ -414,12 +434,13 @@ SyntaxError: Unexpected token } in JSON at position 1234 **Solution:** Make sure you added the language to both arrays in `i18n.ts`: + ```typescript export const supportedLanguages = ['en', 'de', 'fr']; // ← Add here export const languageNames = { - en: 'English', - de: 'Deutsch', - fr: 'Français', // ← And here + en: 'English', + de: 'Deutsch', + fr: 'Français', // ← And here }; ``` @@ -430,6 +451,7 @@ export const languageNames = { When adding a new language, make sure these files are updated: - [ ] `public/locales/{lang}/common.json` - Main translation file +- [ ] `public/locales/{lang}/tools.json` - Tools translation file - [ ] `src/js/i18n/i18n.ts` - Add to `supportedLanguages` and `languageNames` - [ ] Test all pages: homepage, about, contact, FAQ, tool pages - [ ] Test settings modal and shortcuts @@ -470,12 +492,13 @@ Thank you for contributing to BentoPDF! 🎉 Current translation coverage: -| Language | Code | Status | Maintainer | -|----------|------|--------|------------| -| English | `en` | ✅ Complete | Core team | -| German | `de` | 🚧 In Progress | Core team | -| Vietnamese | `vi` | ✅ Complete | Community | -| Your Language | `??` | 🚧 In Progress | You? | +| Language | Code | Status | Maintainer | +| ------------- | ---- | -------------- | ---------- | +| English | `en` | ✅ Complete | Core team | +| German | `de` | 🚧 In Progress | Core team | +| Vietnamese | `vi` | ✅ Complete | Community | +| Indonesian | `id` | ✅ Complete | Community | +| Your Language | `??` | 🚧 In Progress | You? | --- From 5f8e94c41f42868d31f13a7b344b1bcc17a7db2c Mon Sep 17 00:00:00 2001 From: Hanif Naufal Date: Wed, 7 Jan 2026 22:49:05 +0700 Subject: [PATCH 09/39] feat: add Indonesian language support with translations for common phrases and tools --- public/locales/id/common.json | 323 +++++++++++++++++++++ public/locales/id/tools.json | 519 ++++++++++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 257 +++++++++-------- vite.config.ts | 2 +- 4 files changed, 976 insertions(+), 125 deletions(-) create mode 100644 public/locales/id/common.json create mode 100644 public/locales/id/tools.json diff --git a/public/locales/id/common.json b/public/locales/id/common.json new file mode 100644 index 000000000..3dd49f5ad --- /dev/null +++ b/public/locales/id/common.json @@ -0,0 +1,323 @@ +{ + "nav": { + "home": "Beranda", + "about": "Tentang", + "contact": "Kontak", + "licensing": "Lisensi", + "allTools": "Semua Alat", + "openMainMenu": "Buka menu utama", + "language": "Bahasa" + }, + "donation": { + "message": "Suka BentoPDF? Bantu kami menjaganya tetap gratis dan sumber terbuka!", + "button": "Donasi" + }, + "hero": { + "title": " ", + "pdfToolkit": "Toolkit PDF", + "builtForPrivacy": "yang dibuat untuk privasi", + "noSignups": "Tidak Ada Pendaftaran", + "unlimitedUse": "Penggunaan Tak Terbatas", + "worksOffline": "Bekerja Offline ", + "startUsing": "Mulai Menggunakan Sekarang" + }, + "usedBy": { + "title": "Digunakan oleh perusahaan dan orang yang bekerja di" + }, + "features": { + "title": "Mengapa memilih", + "bentoPdf": "BentoPDF?", + "noSignup": { + "title": "Tidak Ada Pendaftaran", + "description": "Mulai seketika, tanpa akun atau email." + }, + "noUploads": { + "title": "Tidak Ada Unggahan", + "description": "100% di sisi klien, file Anda tidak pernah meninggalkan perangkat Anda." + }, + "foreverFree": { + "title": "Gratis Selamanya", + "description": "Semua alat, tidak ada uji coba, tidak ada biaya berlangganan." + }, + "noLimits": { + "title": "Tanpa Batas", + "description": "Gunakan sebanyak yang Anda inginkan, tanpa batas tersembunyi." + }, + "batchProcessing": { + "title": "Pemrosesan Kelompok", + "description": "Tangani PDF tak terbatas dalam satu kali jalan." + }, + "lightningFast": { + "title": "Sangat Cepat", + "description": "Proses PDF secara instan, tanpa menunggu atau penundaan." + } + }, + "tools": { + "title": "Mulai dengan", + "toolsLabel": "Alat", + "subtitle": "Klik alat untuk membuka pengunggah file", + "searchPlaceholder": "Cari alat (contoh, 'pisah', 'organisir'...)", + "backToTools": "Kembali ke Alat", + "firstLoadNotice": "Pemuatan pertama memerlukan waktu sebentar karena kami mengunduh mesin konversi kami. Setelah itu, semua pemuatan akan instan." + }, + "upload": { + "clickToSelect": "Klik untuk memilih file", + "orDragAndDrop": "atau seret dan jatuhkan", + "pdfOrImages": "PDF atau Gambar", + "filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.", + "addMore": "Tambah Lebih Banyak File", + "clearAll": "Hapus Semua" + }, + "loader": { + "processing": "Memproses..." + }, + "alert": { + "title": "Peringatan", + "ok": "OK" + }, + "preview": { + "title": "Pratinjau Dokumen", + "downloadAsPdf": "Unduh sebagai PDF", + "close": "Tutup" + }, + "settings": { + "title": "Pengaturan", + "shortcuts": "Pintasan", + "preferences": "Preferensi", + "displayPreferences": "Preferensi Tampilan", + "searchShortcuts": "Cari pintasan...", + "shortcutsInfo": "Tekan dan tahan tombol untuk mengatur pintasan. Perubahan disimpan otomatis.", + "shortcutsWarning": "⚠️ Hindari pintasan browser umum (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N dll.) karena mungkin tidak bekerja dengan baik.", + "import": "Impor", + "export": "Ekspor", + "resetToDefaults": "Atur Ulang ke Default", + "fullWidthMode": "Mode Lebar Penuh", + "fullWidthDescription": "Gunakan lebar layar penuh untuk semua alat, bukan wadah yang dipusatkan", + "settingsAutoSaved": "Pengaturan disimpan secara otomatis", + "clickToSet": "Klik untuk mengatur", + "pressKeys": "Tekan tombol...", + "warnings": { + "alreadyInUse": "Pintasan Sudah Digunakan", + "assignedTo": "sudah ditugaskan ke:", + "chooseDifferent": "Silakan pilih pintasan yang berbeda.", + "reserved": "Peringatan Pintasan Cadangan", + "commonlyUsed": "biasanya digunakan untuk:", + "unreliable": "Pintasan ini mungkin tidak berfungsi dengan andal atau mungkin bertentangan dengan perilaku browser/sistem.", + "useAnyway": "Apakah Anda ingin menggunakannya saja?", + "resetTitle": "Atur Ulang Pintasan", + "resetMessage": "Apakah Anda yakin ingin mengatur ulang semua pintasan ke default?

Tindakan ini tidak dapat dibatalkan.", + "importSuccessTitle": "Impor Berhasil", + "importSuccessMessage": "Pintasan berhasil diimpor!", + "importFailTitle": "Impor Gagal", + "importFailMessage": "Gagal mengimpor pintasan. Format file tidak valid." + } + }, + "warning": { + "title": "Peringatan", + "cancel": "Batal", + "proceed": "Lanjutkan" + }, + "compliance": { + "title": "Data Anda tidak pernah meninggalkan perangkat Anda", + "weKeep": "Kami menjaga", + "yourInfoSafe": "informasi Anda aman", + "byFollowingStandards": "dengan mengikuti standar keamanan global.", + "processingLocal": "Semua pemrosesan terjadi secara lokal di perangkat Anda.", + "gdpr": { + "title": "Kepatuhan GDPR", + "description": "Melindungi data pribadi dan privasi individu dalam Uni Eropa." + }, + "ccpa": { + "title": "Kepatuhan CCPA", + "description": "Memberikan hak kepada penduduk California atas bagaimana informasi pribadi mereka dikumpulkan, digunakan, dan dibagikan." + }, + "hipaa": { + "title": "Kepatuhan HIPAA", + "description": "Menetapkan perlindungan untuk menangani informasi kesehatan sensitif dalam sistem perawatan kesehatan Amerika Serikat." + } + }, + "faq": { + "title": "Pertanyaan yang Sering Diajukan", + "questions": "Pertanyaan", + "isFree": { + "question": "Apakah BentoPDF benar-benar gratis?", + "answer": "Ya, tentu saja. Semua alat di BentoPDF 100% gratis digunakan, tanpa batas file, tanpa pendaftaran, dan tanpa watermark. Kami percaya semua orang berhak mendapatkan akses ke alat PDF sederhana dan kuat tanpa paywall." + }, + "areFilesSecure": { + "question": "Apakah file saya aman? Di mana file diproses?", + "answer": "File Anda sangat aman karena tidak pernah meninggalkan komputer Anda. Semua pemrosesan terjadi langsung di browser web Anda (sisi klien). Kami tidak pernah mengunggah file Anda ke server, sehingga Anda mempertahankan privasi dan kontrol lengkap atas dokumen Anda." + }, + "platforms": { + "question": "Apakah ini bekerja di Mac, Windows, dan Mobile?", + "answer": "Ya! Karena BentoPDF berjalan sepenuhnya di browser Anda, ini bekerja di sistem operasi apa pun dengan browser web modern, termasuk Windows, macOS, Linux, iOS, dan Android." + }, + "gdprCompliant": { + "question": "Apakah BentoPDF patuh GDPR?", + "answer": "Ya. BentoPDF sepenuhnya patuh GDPR. Karena semua pemrosesan file terjadi secara lokal di browser Anda dan kami tidak pernah mengumpulkan atau mentransmisikan file Anda ke server mana pun, kami tidak memiliki akses ke data Anda. Ini memastikan Anda selalu mengontrol dokumen Anda." + }, + "dataStorage": { + "question": "Apakah Anda menyimpan atau melacak file saya?", + "answer": "Tidak. Kami tidak pernah menyimpan, melacak, atau mencatat file Anda. Semua yang Anda lakukan di BentoPDF terjadi di memori browser Anda dan hilang setelah Anda menutup halaman. Tidak ada unggahan, tidak ada log riwayat, dan tidak ada server yang terlibat." + }, + "different": { + "question": "Apa yang membuat BentoPDF berbeda dari alat PDF lainnya?", + "answer": "Kebanyakan alat PDF mengunggah file Anda ke server untuk diproses. BentoPDF nggak pernah begitu. Kami pakai teknologi web modern dan aman buat memproses file Anda langsung di browser Anda. Ini berarti performa lebih cepat, privasi lebih kuat, dan ketenangan pikiran." + }, + "browserBased": { + "question": "Bagaimana pemrosesan berbasis browser menjaga keamanan saya?", + "answer": "Dengan berjalan sepenuhnya di dalam browser Anda, BentoPDF memastikan file Anda tidak pernah meninggalkan perangkat Anda. Ini menghilangkan risiko peretasan server, pelanggaran data, atau akses tidak sah. File Anda tetap milik Anda—selalu." + }, + "analytics": { + "question": "Apakah Anda menggunakan cookie atau analitik untuk melacak saya?", + "answer": "Kami peduli dengan privasi Anda. BentoPDF tidak melacak informasi pribadi. Kami menggunakan Simple Analytics hanya untuk melihat jumlah kunjungan anonim. Ini berarti kami dapat mengetahui berapa banyak pengguna yang mengunjungi situs kami, tetapi kami tidak pernah tahu siapa Anda. Simple Analytics sepenuhnya patuh GDPR dan menghormati privasi Anda." + } + }, + "testimonials": { + "title": "Apa Kata", + "users": "Pengguna", + "say": "Kami" + }, + "support": { + "title": "Suka Karya Saya?", + "description": "BentoPDF adalah proyek yang dibuat dengan sepenuh hati, untuk menyediakan toolkit PDF gratis, pribadi, dan kuat untuk semua orang. Kalau kamu merasa ini berguna, pertimbangkan untuk mendukung pengembangannya. Setiap secangkir kopi membantu!", + "buyMeCoffee": "Beli Kopi untuk Saya" + }, + "footer": { + "copyright": "© 2025 BentoPDF. Hak cipta dilindungi.", + "version": "Versi", + "company": "Perusahaan", + "aboutUs": "Tentang Kami", + "faqLink": "FAQ", + "contactUs": "Hubungi Kami", + "legal": "Hukum", + "termsAndConditions": "Syarat dan Ketentuan", + "privacyPolicy": "Kebijakan Privasi", + "followUs": "Ikuti Kami" + }, + "merge": { + "title": "Gabung PDF", + "description": "Gabungkan file utuh, atau pilih halaman tertentu untuk digabungkan ke dalam dokumen baru.", + "fileMode": "Mode File", + "pageMode": "Mode Halaman", + "howItWorks": "Cara kerjanya:", + "fileModeInstructions": [ + "Klik dan seret ikon untuk mengubah urutan file.", + "Di kotak \"Halaman\" untuk setiap file, Anda dapat menentukan rentang (misalnya, \"1-3, 5\") untuk menggabungkan hanya halaman tersebut.", + "Biarkan kotak \"Halaman\" kosong untuk menyertakan semua halaman dari file tersebut." + ], + "pageModeInstructions": [ + "Semua halaman dari PDF yang Anda unggah ditampilkan di bawah.", + "Cukup seret dan jatuhkan thumbnail halaman satu per satu untuk membuat urutan yang tepat sesuai keinginan kamu untuk file baru." + ], + "mergePdfs": "Gabung PDF" + }, + "common": { + "page": "Halaman", + "pages": "Halaman", + "of": "dari", + "download": "Unduh", + "cancel": "Batal", + "save": "Simpan", + "delete": "Hapus", + "edit": "Edit", + "add": "Tambah", + "remove": "Hapus", + "loading": "Memuat...", + "error": "Kesalahan", + "success": "Berhasil", + "file": "File", + "files": "File" + }, + "about": { + "hero": { + "title": "Kami percaya alat PDF harus", + "subtitle": "cepat, pribadi, dan gratis.", + "noCompromises": "Tidak ada kompromi." + }, + "mission": { + "title": "Misi Kami", + "description": "Untuk menyediakan toolbox PDF paling komprehensif yang menghormati privasi Anda dan tidak pernah meminta pembayaran. Kami percaya alat dokumen penting harus dapat diakses oleh semua orang, di mana saja, tanpa hambatan." + }, + "philosophy": { + "label": "Filosofi Inti Kami", + "title": "Privasi Yang Utama. Selalu.", + "description": "Di era di mana data adalah komoditas, kami mengambil pendekatan berbeda. Semua pemrosesan untuk alat Bentopdf terjadi secara lokal di browser Anda. Ini berarti file Anda tidak pernah menyentuh server kami, kami tidak pernah melihat dokumen Anda, dan kami tidak melacak apa yang Anda lakukan. Dokumen Anda tetap utuh dan privat secara tegas. Ini bukan hanya fitur; ini adalah fondasi kami." + }, + "whyBentopdf": { + "title": "Mengapa", + "speed": { + "title": "Dibuat untuk Kecepatan", + "description": "Tidak ada menunggu unggahan atau unduhan ke server. Dengan memproses file langsung di browser Anda menggunakan teknologi web modern seperti WebAssembly, kami menawarkan kecepatan yang tak tertandingi untuk semua alat kami." + }, + "free": { + "title": "Sepenuhnya Gratis", + "description": "Tidak ada uji coba, tidak ada langganan, tidak ada biaya tersembunyi, dan tidak ada fitur \"premium\" yang ditahan sebagai sandera. Kami percaya alat PDF yang kuat harus menjadi utilitas publik, bukan pusat keuntungan." + }, + "noAccount": { + "title": "Tidak Perlu Akun", + "description": "Langsung mulai pakai alat apa saja. Kami tidak perlu email Anda, kata sandi, atau informasi pribadi apa pun. Alur kerja Anda harus lancar dan anonim." + }, + "openSource": { + "title": "Semangat Sumber Terbuka", + "description": "Dibangun dengan transparansi sebagai prioritas. Kami manfaatkan pustaka open-source yang luar biasa seperti PDF-lib dan PDF.js, dan percaya pada upaya yang didorong komunitas untuk membuat alat-alat kuat bisa diakses semua orang." + } + }, + "cta": { + "title": "Siap untuk memulai?", + "description": "Bergabunglah dengan ribuan pengguna yang percaya Bentopdf untuk kebutuhan dokumen harian mereka. Rasakan bedanya privasi dan performa yang ditawarkan.", + "button": "Jelajahi Semua Alat" + } + }, + "contact": { + "title": "Hubungi Kami", + "subtitle": "Kami senang mendengar dari Anda. Apakah Anda memiliki pertanyaan, umpan balik, atau permintaan fitur, jangan ragu untuk menghubungi kami.", + "email": "Anda dapat menghubungi kami secara langsung melalui email di:" + }, + "licensing": { + "title": "Lisensi untuk", + "subtitle": "Pilih lisensi yang sesuai dengan kebutuhan Anda." + }, + "multiTool": { + "uploadPdfs": "Unggah PDF", + "upload": "Unggah", + "addBlankPage": "Tambah Halaman Kosong", + "edit": "Edit:", + "undo": "Undo", + "redo": "Redo", + "reset": "Atur Ulang", + "selection": "Pilihan:", + "selectAll": "Pilih Semua", + "deselectAll": "Batalkan Pilih Semua", + "rotate": "Putar:", + "rotateLeft": "Kiri", + "rotateRight": "Kanan", + "transform": "Transform:", + "duplicate": "Duplikat", + "split": "Pisah", + "clear": "Hapus:", + "delete": "Hapus", + "download": "Unduh:", + "downloadSelected": "Unduh yang Dipilih", + "exportPdf": "Ekspor PDF", + "uploadPdfFiles": "Pilih File PDF", + "dragAndDrop": "Seret dan jatuhkan file PDF di sini, atau klik untuk memilih", + "selectFiles": "Pilih File", + "renderingPages": "Merender halaman...", + "actions": { + "duplicatePage": "Duplikat halaman ini", + "deletePage": "Hapus halaman ini", + "insertPdf": "Sisipkan PDF setelah halaman ini", + "toggleSplit": "Alihkan pisah setelah halaman ini" + }, + "pleaseWait": "Harap Tunggu", + "pagesRendering": "Halaman masih dirender. Harap tunggu...", + "noPagesSelected": "Tidak Ada Halaman yang Dipilih", + "selectOnePage": "Silakan pilih setidaknya satu halaman untuk diunduh.", + "noPages": "Tidak Ada Halaman", + "noPagesToExport": "Tidak ada halaman untuk diekspor.", + "renderingTitle": "Merender pratinjau halaman", + "errorRendering": "Gagal merender thumbnail halaman", + "error": "Kesalahan", + "failedToLoad": "Gagal memuat" + } +} diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json new file mode 100644 index 000000000..f1e27ec23 --- /dev/null +++ b/public/locales/id/tools.json @@ -0,0 +1,519 @@ +{ + "categories": { + "popularTools": "Alat Populer", + "editAnnotate": "Edit & Anotasi", + "convertToPdf": "Konversi ke PDF", + "convertFromPdf": "Konversi dari PDF", + "organizeManage": "Atur & Kelola", + "optimizeRepair": "Optimalkan & Perbaiki", + "securePdf": "Amankan PDF" + }, + "pdfMultiTool": { + "name": "Alat Multi PDF", + "subtitle": "Gabung, Pisah, Atur, Hapus, Putar, Tambah Halaman Kosong, Ekstrak dan Duplikat dalam antarmuka terpadu." + }, + "mergePdf": { + "name": "Gabung PDF", + "subtitle": "Gabungkan beberapa PDF menjadi satu file. Mempertahankan Bookmark." + }, + "splitPdf": { + "name": "Pisah PDF", + "subtitle": "Ekstrak rentang halaman ke PDF baru." + }, + "compressPdf": { + "name": "Kompres PDF", + "subtitle": "Kurangi ukuran file PDF Anda.", + "algorithmLabel": "Algoritma Kompresi", + "condense": "Kondensasi (Direkomendasikan)", + "photon": "Photon (Untuk PDF yang penuh Foto)", + "condenseInfo": "Kondensasi menggunakan kompresi lanjutan: menghapus bobot mati, mengoptimalkan gambar, subset font. Terbaik untuk sebagian besar PDF.", + "photonInfo": "Photon mengkonversi halaman ke gambar. Gunakan untuk PDF penuh foto/terpindai.", + "photonWarning": "Peringatan: Teks akan menjadi tidak dapat dipilih dan tautan akan berhenti berfungsi.", + "levelLabel": "Tingkat Kompresi", + "light": "Ringan (Pertahankan Kualitas)", + "balanced": "Seimbang (Direkomendasikan)", + "aggressive": "Agresif (File Lebih Kecil)", + "extreme": "Ekstrem (Kompresi Maksimum)", + "grayscale": "Konversi ke Grayscale", + "grayscaleHint": "Mengurangi ukuran file dengan menghapus informasi warna", + "customSettings": "Pengaturan Kustom", + "customSettingsHint": "Sesuaikan parameter kompresi:", + "outputQuality": "Kualitas Output", + "resizeImagesTo": "Ubah Ukuran Gambar Ke", + "onlyProcessAbove": "Hanya Proses Di Atas", + "removeMetadata": "Hapus metadata", + "subsetFonts": "Subset font (hapus glyph yang tidak digunakan)", + "removeThumbnails": "Hapus thumbnail tersemat", + "compressButton": "Kompres PDF" + }, + "pdfEditor": { + "name": "Editor PDF", + "subtitle": "Anotasi, sorot, redaksi, komentar, tambah bentuk/gambar, cari, dan lihat PDF." + }, + "jpgToPdf": { + "name": "JPG ke PDF", + "subtitle": "Buat PDF dari gambar JPG, JPEG, dan JPEG2000 (JP2/JPX)." + }, + "signPdf": { + "name": "Tanda Tangan PDF", + "subtitle": "Gambar, ketik, atau unggah tanda tangan Anda." + }, + "cropPdf": { + "name": "Potong PDF", + "subtitle": "Potong margin setiap halaman di PDF Anda." + }, + "extractPages": { + "name": "Ekstrak Halaman", + "subtitle": "Simpan pilihan halaman sebagai file baru." + }, + "duplicateOrganize": { + "name": "Duplikat & Atur", + "subtitle": "Duplikat, susun ulang, dan hapus halaman." + }, + "deletePages": { + "name": "Hapus Halaman", + "subtitle": "Hapus halaman tertentu dari dokumen Anda." + }, + "editBookmarks": { + "name": "Edit Bookmark", + "subtitle": "Tambah, edit, impor, hapus dan ekstrak bookmark PDF." + }, + "tableOfContents": { + "name": "Daftar Isi", + "subtitle": "Hasilkan halaman daftar isi dari bookmark PDF." + }, + "pageNumbers": { + "name": "Nomor Halaman", + "subtitle": "Sisipkan nomor halaman ke dokumen Anda." + }, + "addWatermark": { + "name": "Tambah Watermark", + "subtitle": "Cap teks atau gambar di atas halaman PDF Anda." + }, + "headerFooter": { + "name": "Header & Footer", + "subtitle": "Tambah teks ke bagian atas dan bawah halaman." + }, + "invertColors": { + "name": "Balik Warna", + "subtitle": "Buat versi \"mode gelap\" dari PDF Anda." + }, + "backgroundColor": { + "name": "Warna Latar Belakang", + "subtitle": "Ubah warna latar belakang PDF Anda." + }, + "changeTextColor": { + "name": "Ubah Warna Teks", + "subtitle": "Ubah warna teks di PDF Anda." + }, + "addStamps": { + "name": "Tambah Stempel", + "subtitle": "Tambah stempel gambar ke PDF Anda menggunakan toolbar anotasi.", + "usernameLabel": "Nama Pengguna Stempel", + "usernamePlaceholder": "Masukkan nama Anda (untuk stempel)", + "usernameHint": "Nama ini akan muncul pada stempel yang Anda buat." + }, + "removeAnnotations": { + "name": "Hapus Anotasi", + "subtitle": "Hapus komentar, sorotan, dan tautan." + }, + "pdfFormFiller": { + "name": "Pengisi Formulir PDF", + "subtitle": "Isi formulir langsung di browser. Juga mendukung formulir XFA." + }, + "createPdfForm": { + "name": "Buat Formulir PDF", + "subtitle": "Buat formulir PDF yang dapat diisi dengan bidang teks drag-and-drop." + }, + "removeBlankPages": { + "name": "Hapus Halaman Kosong", + "subtitle": "Deteksi dan hapus halaman kosong secara otomatis." + }, + "imageToPdf": { + "name": "Gambar ke PDF", + "subtitle": "Konversi JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP ke PDF." + }, + "pngToPdf": { + "name": "PNG ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar PNG." + }, + "webpToPdf": { + "name": "WebP ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar WebP." + }, + "svgToPdf": { + "name": "SVG ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar SVG." + }, + "bmpToPdf": { + "name": "BMP ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar BMP." + }, + "heicToPdf": { + "name": "HEIC ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar HEIC." + }, + "tiffToPdf": { + "name": "TIFF ke PDF", + "subtitle": "Buat PDF dari satu atau lebih gambar TIFF." + }, + "textToPdf": { + "name": "Teks ke PDF", + "subtitle": "Konversi file teks biasa menjadi PDF." + }, + "jsonToPdf": { + "name": "JSON ke PDF", + "subtitle": "Konversi file JSON ke format PDF." + }, + "pdfToJpg": { + "name": "PDF ke JPG", + "subtitle": "Konversi setiap halaman PDF menjadi gambar JPG." + }, + "pdfToPng": { + "name": "PDF ke PNG", + "subtitle": "Konversi setiap halaman PDF menjadi gambar PNG." + }, + "pdfToWebp": { + "name": "PDF ke WebP", + "subtitle": "Konversi setiap halaman PDF menjadi gambar WebP." + }, + "pdfToBmp": { + "name": "PDF ke BMP", + "subtitle": "Konversi setiap halaman PDF menjadi gambar BMP." + }, + "pdfToTiff": { + "name": "PDF ke TIFF", + "subtitle": "Konversi setiap halaman PDF menjadi gambar TIFF." + }, + "pdfToGreyscale": { + "name": "PDF ke Skala Abu-abu", + "subtitle": "Konversi semua warna ke hitam dan putih." + }, + "pdfToJson": { + "name": "PDF ke JSON", + "subtitle": "Konversi file PDF ke format JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Buat PDF dapat dicari dan disalin." + }, + "alternateMix": { + "name": "Alternatif & Campur Halaman", + "subtitle": "Gabung PDF dengan menggantikan halaman dari setiap PDF. Mempertahankan Bookmark." + }, + "addAttachments": { + "name": "Tambah Lampiran", + "subtitle": "Sematkan satu atau lebih file ke PDF Anda." + }, + "extractAttachments": { + "name": "Ekstrak Lampiran", + "subtitle": "Ekstrak semua file tersemat dari PDF sebagai ZIP." + }, + "editAttachments": { + "name": "Edit Lampiran", + "subtitle": "Lihat atau hapus lampiran di PDF Anda." + }, + "dividePages": { + "name": "Bagi Halaman", + "subtitle": "Bagi halaman secara horizontal atau vertikal." + }, + "addBlankPage": { + "name": "Tambah Halaman Kosong", + "subtitle": "Sisipkan halaman kosong di mana saja di PDF Anda." + }, + "reversePages": { + "name": "Balik Halaman", + "subtitle": "Balik urutan semua halaman di dokumen Anda." + }, + "rotatePdf": { + "name": "Putar PDF", + "subtitle": "Putar halaman dalam kenaikan 90 derajat." + }, + "rotateCustom": { + "name": "Putar dengan Derajat Kustom", + "subtitle": "Putar halaman dengan sudut kustom apa pun." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Susun beberapa halaman ke satu lembar." + }, + "combineToSinglePage": { + "name": "Gabung ke Halaman Tunggal", + "subtitle": "Jahit semua halaman menjadi satu gulungan berkelanjutan." + }, + "viewMetadata": { + "name": "Lihat Metadata", + "subtitle": "Periksa properti tersembunyi PDF Anda." + }, + "editMetadata": { + "name": "Edit Metadata", + "subtitle": "Ubah penulis, judul, dan properti lainnya." + }, + "pdfsToZip": { + "name": "PDF ke ZIP", + "subtitle": "Paket beberapa file PDF ke arsip ZIP." + }, + "comparePdfs": { + "name": "Bandingkan PDF", + "subtitle": "Bandingkan dua PDF berdampingan." + }, + "posterizePdf": { + "name": "Posterisasi PDF", + "subtitle": "Pisah halaman besar menjadi beberapa halaman lebih kecil." + }, + "fixPageSize": { + "name": "Perbaiki Ukuran Halaman", + "subtitle": "Standarkan semua halaman ke ukuran seragam." + }, + "linearizePdf": { + "name": "Linierisasi PDF", + "subtitle": "Optimalkan PDF untuk tampilan web cepat." + }, + "pageDimensions": { + "name": "Dimensi Halaman", + "subtitle": "Analisis ukuran halaman, orientasi, dan unit." + }, + "removeRestrictions": { + "name": "Hapus Pembatasan", + "subtitle": "Hapus perlindungan kata sandi dan pembatasan keamanan yang terkait dengan file PDF yang ditandatangani secara digital." + }, + "repairPdf": { + "name": "Perbaiki PDF", + "subtitle": "Pulihkan data dari file PDF yang rusak atau rusak." + }, + "encryptPdf": { + "name": "Enkripsi PDF", + "subtitle": "Kunci PDF Anda dengan menambahkan kata sandi." + }, + "sanitizePdf": { + "name": "Sanitasi PDF", + "subtitle": "Hapus metadata, anotasi, skrip, dan lainnya." + }, + "decryptPdf": { + "name": "Dekripsi PDF", + "subtitle": "Buka kunci PDF dengan menghapus perlindungan kata sandi." + }, + "flattenPdf": { + "name": "Ratakan PDF", + "subtitle": "Buat bidang formulir dan anotasi tidak dapat diedit." + }, + "removeMetadata": { + "name": "Hapus Metadata", + "subtitle": "Hapus data tersembunyi dari PDF Anda." + }, + "changePermissions": { + "name": "Ubah Izin", + "subtitle": "Atur atau ubah izin pengguna pada PDF." + }, + "odtToPdf": { + "name": "ODT ke PDF", + "subtitle": "Konversi file OpenDocument Text ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File ODT", + "convertButton": "Konversi ke PDF" + }, + "csvToPdf": { + "name": "CSV ke PDF", + "subtitle": "Konversi file spreadsheet CSV ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File CSV", + "convertButton": "Konversi ke PDF" + }, + "rtfToPdf": { + "name": "RTF ke PDF", + "subtitle": "Konversi dokumen Rich Text Format ke PDF. Mendukung beberapa file.", + "acceptedFormats": "File RTF", + "convertButton": "Konversi ke PDF" + }, + "wordToPdf": { + "name": "Word ke PDF", + "subtitle": "Konversi dokumen Word (DOCX, DOC, ODT, RTF) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File DOCX, DOC, ODT, RTF", + "convertButton": "Konversi ke PDF" + }, + "excelToPdf": { + "name": "Excel ke PDF", + "subtitle": "Konversi spreadsheet Excel (XLSX, XLS, ODS, CSV) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File XLSX, XLS, ODS, CSV", + "convertButton": "Konversi ke PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint ke PDF", + "subtitle": "Konversi presentasi PowerPoint (PPTX, PPT, ODP) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File PPTX, PPT, ODP", + "convertButton": "Konversi ke PDF" + }, + "markdownToPdf": { + "name": "Markdown ke PDF", + "subtitle": "Tulis atau tempel Markdown dan ekspor sebagai PDF yang diformat dengan indah.", + "paneMarkdown": "Markdown", + "panePreview": "Pratinjau", + "btnUpload": "Unggah", + "btnSyncScroll": "Sinkron Gulir", + "btnSettings": "Pengaturan", + "btnExportPdf": "Ekspor PDF", + "settingsTitle": "Pengaturan Markdown", + "settingsPreset": "Preset", + "presetDefault": "Default (seperti GFM)", + "presetCommonmark": "CommonMark (ketat)", + "presetZero": "Minimal (tanpa fitur)", + "settingsOptions": "Opsi Markdown", + "optAllowHtml": "Izinkan tag HTML", + "optBreaks": "Konversi baris baru ke
", + "optLinkify": "Konversi otomatis URL ke tautan", + "optTypographer": "Typographer (kutipan cerdas, dll.)" + }, + "pdfBooklet": { + "name": "Buku Kecil PDF", + "subtitle": "Susun ulang halaman untuk pencetakan buku kecil dua sisi. Lipat dan jahit untuk membuat buku kecil.", + "howItWorks": "Cara kerjanya:", + "step1": "Unggah file PDF.", + "step2": "Halaman akan disusun ulang dalam urutan buku kecil.", + "step3": "Cetak dua sisi, balik di tepi pendek, lipat dan jahit.", + "paperSize": "Ukuran Kertas", + "orientation": "Orientasi", + "portrait": "Potret", + "landscape": "Lanskap", + "pagesPerSheet": "Halaman per Lembar", + "createBooklet": "Buat Buku Kecil", + "processing": "Memproses...", + "pageCount": "Jumlah halaman akan diisi ke kelipatan 4 jika diperlukan." + }, + "xpsToPdf": { + "name": "XPS ke PDF", + "subtitle": "Konversi dokumen XPS/OXPS ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File XPS, OXPS", + "convertButton": "Konversi ke PDF" + }, + "mobiToPdf": { + "name": "MOBI ke PDF", + "subtitle": "Konversi e-book MOBI ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File MOBI", + "convertButton": "Konversi ke PDF" + }, + "epubToPdf": { + "name": "EPUB ke PDF", + "subtitle": "Konversi e-book EPUB ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File EPUB", + "convertButton": "Konversi ke PDF" + }, + "fb2ToPdf": { + "name": "FB2 ke PDF", + "subtitle": "Konversi e-book FictionBook (FB2) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File FB2", + "convertButton": "Konversi ke PDF" + }, + "cbzToPdf": { + "name": "CBZ ke PDF", + "subtitle": "Konversi arsip buku komik (CBZ/CBR) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File CBZ, CBR", + "convertButton": "Konversi ke PDF" + }, + "wpdToPdf": { + "name": "WPD ke PDF", + "subtitle": "Konversi dokumen WordPerfect (WPD) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File WPD", + "convertButton": "Konversi ke PDF" + }, + "wpsToPdf": { + "name": "WPS ke PDF", + "subtitle": "Konversi dokumen WPS Office ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File WPS", + "convertButton": "Konversi ke PDF" + }, + "xmlToPdf": { + "name": "XML ke PDF", + "subtitle": "Konversi dokumen XML ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File XML", + "convertButton": "Konversi ke PDF" + }, + "pagesToPdf": { + "name": "Pages ke PDF", + "subtitle": "Konversi dokumen Apple Pages ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File Pages", + "convertButton": "Konversi ke PDF" + }, + "odgToPdf": { + "name": "ODG ke PDF", + "subtitle": "Konversi file OpenDocument Graphics (ODG) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File ODG", + "convertButton": "Konversi ke PDF" + }, + "odsToPdf": { + "name": "ODS ke PDF", + "subtitle": "Konversi file OpenDocument Spreadsheet (ODS) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File ODS", + "convertButton": "Konversi ke PDF" + }, + "odpToPdf": { + "name": "ODP ke PDF", + "subtitle": "Konversi file OpenDocument Presentation (ODP) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File ODP", + "convertButton": "Konversi ke PDF" + }, + "pubToPdf": { + "name": "PUB ke PDF", + "subtitle": "Konversi file Microsoft Publisher (PUB) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File PUB", + "convertButton": "Konversi ke PDF" + }, + "vsdToPdf": { + "name": "VSD ke PDF", + "subtitle": "Konversi file Microsoft Visio (VSD, VSDX) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File VSD, VSDX", + "convertButton": "Konversi ke PDF" + }, + "psdToPdf": { + "name": "PSD ke PDF", + "subtitle": "Konversi file Adobe Photoshop (PSD) ke format PDF. Mendukung beberapa file.", + "acceptedFormats": "File PSD", + "convertButton": "Konversi ke PDF" + }, + "pdfToSvg": { + "name": "PDF ke SVG", + "subtitle": "Konversi setiap halaman file PDF menjadi grafik vektor skalabel (SVG) untuk kualitas sempurna di ukuran apa pun." + }, + "extractTables": { + "name": "Ekstrak Tabel PDF", + "subtitle": "Ekstrak tabel dari file PDF dan ekspor sebagai CSV, JSON, atau Markdown." + }, + "pdfToCsv": { + "name": "PDF ke CSV", + "subtitle": "Ekstrak tabel dari PDF dan konversi ke format CSV." + }, + "pdfToExcel": { + "name": "PDF ke Excel", + "subtitle": "Ekstrak tabel dari PDF dan konversi ke format Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF ke Teks", + "subtitle": "Ekstrak teks dari file PDF dan simpan sebagai teks biasa (.txt). Mendukung beberapa file.", + "note": "Alat ini bekerja HANYA dengan PDF yang dibuat secara digital. Untuk dokumen terpindai atau PDF berbasis gambar, gunakan alat OCR PDF kami sebagai gantinya.", + "convertButton": "Ekstrak Teks" + }, + "digitalSignPdf": { + "name": "Tanda Tangan Digital PDF", + "pageTitle": "Tanda Tangan Digital PDF - Tambah Tanda Tangan Kriptografi | BentoPDF", + "subtitle": "Tambah tanda tangan digital kriptografi ke PDF Anda menggunakan sertifikat X.509. Mendukung format PKCS#12 (.pfx, .p12) dan PEM. Kunci pribadi Anda tidak pernah meninggalkan browser Anda.", + "certificateSection": "Sertifikat", + "uploadCert": "Unggah sertifikat (.pfx, .p12)", + "certPassword": "Kata Sandi Sertifikat", + "certPasswordPlaceholder": "Masukkan kata sandi sertifikat", + "certInfo": "Informasi Sertifikat", + "certSubject": "Subjek", + "certIssuer": "Penerbit", + "certValidity": "Valid", + "signatureDetails": "Detail Tanda Tangan (Opsional)", + "reason": "Alasan", + "reasonPlaceholder": "misalnya, Saya menyetujui dokumen ini", + "location": "Lokasi", + "locationPlaceholder": "misalnya, Jakarta, Indonesia", + "contactInfo": "Info Kontak", + "contactPlaceholder": "misalnya, email@contoh.com", + "applySignature": "Terapkan Tanda Tangan Digital", + "successMessage": "PDF berhasil ditandatangani! Tanda tangan dapat diverifikasi di pembaca PDF apa pun." + }, + "validateSignaturePdf": { + "name": "Validasi Tanda Tangan PDF", + "pageTitle": "Validasi Tanda Tangan PDF - Verifikasi Tanda Tangan Digital | BentoPDF", + "subtitle": "Verifikasi tanda tangan digital di file PDF Anda. Periksa validitas sertifikat, lihat detail penandatangan, dan konfirmasi integritas dokumen. Semua pemrosesan terjadi di browser Anda." + } +} diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index fab9d0705..8a6db1a6f 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -3,155 +3,164 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // Supported languages -export const supportedLanguages = ['en', 'de', 'zh', 'vi', 'it'] as const; +export const supportedLanguages = ['en', 'de', 'zh', 'vi', 'it', 'id'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { - en: 'English', - de: 'Deutsch', - zh: '中文', - vi: 'Tiếng Việt', - it: 'Italiano', + en: 'English', + de: 'Deutsch', + zh: '中文', + vi: 'Tiếng Việt', + it: 'Italiano', + id: 'Indonesia', }; export const getLanguageFromUrl = (): SupportedLanguage => { - const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi|it)(?:\/|$)/); - if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) { - return langMatch[1] as SupportedLanguage; - } - const storedLang = localStorage.getItem('i18nextLng'); - if (storedLang && supportedLanguages.includes(storedLang as SupportedLanguage)) { - return storedLang as SupportedLanguage; - } - - return 'en'; + const path = window.location.pathname; + const langMatch = path.match(/^\/(en|de|zh|vi|it|id)(?:\/|$)/); + if ( + langMatch && + supportedLanguages.includes(langMatch[1] as SupportedLanguage) + ) { + return langMatch[1] as SupportedLanguage; + } + const storedLang = localStorage.getItem('i18nextLng'); + if ( + storedLang && + supportedLanguages.includes(storedLang as SupportedLanguage) + ) { + return storedLang as SupportedLanguage; + } + + return 'en'; }; let initialized = false; export const initI18n = async (): Promise => { - if (initialized) return i18next; - - const currentLang = getLanguageFromUrl(); - - await i18next - .use(HttpBackend) - .use(LanguageDetector) - .init({ - lng: currentLang, - fallbackLng: 'en', - supportedLngs: supportedLanguages as unknown as string[], - ns: ['common', 'tools'], - defaultNS: 'common', - backend: { - loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`, - }, - detection: { - order: ['path', 'localStorage', 'navigator'], - lookupFromPathIndex: 0, - caches: ['localStorage'], - }, - interpolation: { - escapeValue: false, - }, - }); - - initialized = true; - return i18next; + if (initialized) return i18next; + + const currentLang = getLanguageFromUrl(); + + await i18next + .use(HttpBackend) + .use(LanguageDetector) + .init({ + lng: currentLang, + fallbackLng: 'en', + supportedLngs: supportedLanguages as unknown as string[], + ns: ['common', 'tools'], + defaultNS: 'common', + backend: { + loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`, + }, + detection: { + order: ['path', 'localStorage', 'navigator'], + lookupFromPathIndex: 0, + caches: ['localStorage'], + }, + interpolation: { + escapeValue: false, + }, + }); + + initialized = true; + return i18next; }; export const t = (key: string, options?: Record): string => { - return i18next.t(key, options); + return i18next.t(key, options); }; export const changeLanguage = (lang: SupportedLanguage): void => { - if (!supportedLanguages.includes(lang)) return; - - const currentPath = window.location.pathname; - const currentLang = getLanguageFromUrl(); - - let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi|it)\//)) { - newPath = currentPath.replace(/^\/(en|de|zh|vi|it)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|zh|vi|it)$/)) { - newPath = `/${lang}`; - } else { - newPath = `/${lang}${currentPath}`; - } - - const newUrl = newPath + window.location.search + window.location.hash; - window.location.href = newUrl; + if (!supportedLanguages.includes(lang)) return; + + const currentPath = window.location.pathname; + const currentLang = getLanguageFromUrl(); + + let newPath: string; + if (currentPath.match(/^\/(en|de|zh|vi|it|id)\//)) { + newPath = currentPath.replace(/^\/(en|de|zh|vi|it|id)\//, `/${lang}/`); + } else if (currentPath.match(/^\/(en|de|zh|vi|it|id)$/)) { + newPath = `/${lang}`; + } else { + newPath = `/${lang}${currentPath}`; + } + + const newUrl = newPath + window.location.search + window.location.hash; + window.location.href = newUrl; }; // Apply translations to all elements with data-i18n attribute export const applyTranslations = (): void => { - document.querySelectorAll('[data-i18n]').forEach((element) => { - const key = element.getAttribute('data-i18n'); - if (key) { - const translation = t(key); - if (translation && translation !== key) { - element.textContent = translation; - } - } - }); - - document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { - const key = element.getAttribute('data-i18n-placeholder'); - if (key && element instanceof HTMLInputElement) { - const translation = t(key); - if (translation && translation !== key) { - element.placeholder = translation; - } - } - }); - - document.querySelectorAll('[data-i18n-title]').forEach((element) => { - const key = element.getAttribute('data-i18n-title'); - if (key) { - const translation = t(key); - if (translation && translation !== key) { - (element as HTMLElement).title = translation; - } - } - }); + document.querySelectorAll('[data-i18n]').forEach((element) => { + const key = element.getAttribute('data-i18n'); + if (key) { + const translation = t(key); + if (translation && translation !== key) { + element.textContent = translation; + } + } + }); + + document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { + const key = element.getAttribute('data-i18n-placeholder'); + if (key && element instanceof HTMLInputElement) { + const translation = t(key); + if (translation && translation !== key) { + element.placeholder = translation; + } + } + }); + + document.querySelectorAll('[data-i18n-title]').forEach((element) => { + const key = element.getAttribute('data-i18n-title'); + if (key) { + const translation = t(key); + if (translation && translation !== key) { + (element as HTMLElement).title = translation; + } + } + }); - document.documentElement.lang = i18next.language; + document.documentElement.lang = i18next.language; }; export const rewriteLinks = (): void => { - const currentLang = getLanguageFromUrl(); - if (currentLang === 'en') return; - - const links = document.querySelectorAll('a[href]'); - links.forEach((link) => { - const href = link.getAttribute('href'); - if (!href) return; - - if (href.startsWith('http') || - href.startsWith('mailto:') || - href.startsWith('tel:') || - href.startsWith('#') || - href.startsWith('javascript:')) { - return; - } - - if (href.match(/^\/(en|de|zh|vi|it)\//)) { - return; - } - let newHref: string; - if (href.startsWith('/')) { - newHref = `/${currentLang}${href}`; - } else if (href.startsWith('./')) { - newHref = href.replace('./', `/${currentLang}/`); - } else if (href === '/' || href === '') { - newHref = `/${currentLang}/`; - } else { - newHref = `/${currentLang}/${href}`; - } - - link.setAttribute('href', newHref); - }); + const currentLang = getLanguageFromUrl(); + if (currentLang === 'en') return; + + const links = document.querySelectorAll('a[href]'); + links.forEach((link) => { + const href = link.getAttribute('href'); + if (!href) return; + + if ( + href.startsWith('http') || + href.startsWith('mailto:') || + href.startsWith('tel:') || + href.startsWith('#') || + href.startsWith('javascript:') + ) { + return; + } + + if (href.match(/^\/(en|de|zh|vi|it|id)\//)) { + return; + } + let newHref: string; + if (href.startsWith('/')) { + newHref = `/${currentLang}${href}`; + } else if (href.startsWith('./')) { + newHref = href.replace('./', `/${currentLang}/`); + } else if (href === '/' || href === '') { + newHref = `/${currentLang}/`; + } else { + newHref = `/${currentLang}/${href}`; + } + + link.setAttribute('href', newHref); + }); }; export default i18next; diff --git a/vite.config.ts b/vite.config.ts index d59dbfd74..43e70788f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ function pagesRewritePlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url?.split('?')[0] || ''; - const langMatch = url.match(/^\/(en|de|zh|vi|it)(\/.*)?$/); + const langMatch = url.match(/^\/(en|de|zh|vi|it|id)(\/.*)?$/); if (langMatch) { const lang = langMatch[1]; const restOfPath = langMatch[2] || '/'; From 2ee1dbf542d09a954c595cda8114d40312abd262 Mon Sep 17 00:00:00 2001 From: Hanif Naufal Date: Wed, 7 Jan 2026 23:02:41 +0700 Subject: [PATCH 10/39] feat: update translation guide and configuration for Indonesian language support --- TRANSLATION.md | 11 ++++++++++- docs/self-hosting/hostinger.md | 9 ++++----- nginx.conf | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 6ad2587a9..57dc499bf 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -117,7 +117,16 @@ export const languageNames: Record = { }; ``` -### Step 4: Test Your Translation +### Step 4: Update Vite Configuration + +In `vite.config.ts`, ensure the new language is included in the build: + +```typescript +// Add 'fr' to the language regex +const langMatch = url.match(/^\/(en|de|zh|vi|it|fr)(\/.*)?$/); +``` + +### Step 5: Test Your Translation ```bash # Start the dev server diff --git a/docs/self-hosting/hostinger.md b/docs/self-hosting/hostinger.md index ea8ac5526..8deaf686c 100644 --- a/docs/self-hosting/hostinger.md +++ b/docs/self-hosting/hostinger.md @@ -40,7 +40,6 @@ BASE_URL=/pdf-tools/ npm run build 2. Create the folder in Hostinger: - Go to **File Manager** → **public_html** - Create a new folder: `pdf-tools` - 3. Upload all contents of `dist` to `public_html/pdf-tools/` ## Step 3: Create .htaccess File @@ -64,7 +63,7 @@ RewriteBase / Header always set X-XSS-Protection "1; mode=block" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" - + # REQUIRED for soffice.js (SharedArrayBuffer) Header always set Cross-Origin-Opener-Policy "same-origin" Header always set Cross-Origin-Embedder-Policy "require-corp" @@ -161,8 +160,8 @@ RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] # Language routes -RewriteRule ^(de|en|zh|vi)/(.*)$ /$2 [L] -RewriteRule ^(de|en|zh|vi)/?$ / [L] +RewriteRule ^(en|de|zh|vi|it|id)/(.*)$ /$2 [L] +RewriteRule ^(en|de|zh|vi|it|id)/?$ / [L] # ============================================ # 5.5. DOCS ROUTING (VitePress) @@ -181,7 +180,7 @@ RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ /index.html [L] -ErrorDocument 404 /index.html +ErrorDocument 404 /index.html ``` ## Subdirectory .htaccess Example diff --git a/nginx.conf b/nginx.conf index eb1c1bfe0..f347dc153 100644 --- a/nginx.conf +++ b/nginx.conf @@ -26,7 +26,7 @@ http { root /usr/share/nginx/html; index index.html; - rewrite ^/(en|de|zh|vi|it)/(.*)$ /$2 last; + rewrite ^/(en|de|zh|vi|it|id)/(.*)$ /$2 last; location ~* \.html$ { expires 1h; From 7907f209070ea20fedcf141e432cfb9534e85737 Mon Sep 17 00:00:00 2001 From: Hanif Naufal Date: Thu, 8 Jan 2026 00:09:39 +0700 Subject: [PATCH 11/39] fix: correct Indonesian language name in languageNames mapping --- src/js/i18n/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index 8a6db1a6f..6c05b3643 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -12,7 +12,7 @@ export const languageNames: Record = { zh: '中文', vi: 'Tiếng Việt', it: 'Italiano', - id: 'Indonesia', + id: 'Indonesian', }; export const getLanguageFromUrl = (): SupportedLanguage => { From c58fcec497fc99f3e22cd761698e14912ec48643 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:29:39 +0000 Subject: [PATCH 12/39] @alam00000 has signed the CLA in alam00000/bentopdf#289 --- signatures/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/signatures/cla.json b/signatures/cla.json index 34ffcba2c..42dcea152 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -231,6 +231,14 @@ "created_at": "2026-01-07T16:11:08Z", "repoId": 1074785178, "pullRequestNo": 367 + }, + { + "name": "alam00000", + "id": 50314772, + "comment_id": 3722774353, + "created_at": "2026-01-08T08:29:28Z", + "repoId": 1074785178, + "pullRequestNo": 289 } ] } \ No newline at end of file From 280348763d4b3627f109419b72e630652971dc44 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 8 Jan 2026 21:36:21 +0530 Subject: [PATCH 13/39] feat(email-to-pdf): add inline images, clickable links, and embedded attachments - Add CID inline image support via base64 data URI replacement - Implement clickable link extraction from HTML anchors using regex - Embed email attachments into PDF using pymupdf embfile_add - Reduce font sizes for more compact PDF output (18px subject, 12px body) - Format date with timezone (UTC+HH:MM) while preserving original time - Clean email address formatting (Name (email) instead of ) - Add UI options: page size selector, CC/BCC toggle, attachments toggle --- index.html | 2 - package-lock.json | 41 +- package.json | 4 +- public/locales/de/tools.json | 1042 +++++++++-------- public/locales/en/tools.json | 1042 +++++++++-------- public/locales/id/tools.json | 6 + public/locales/it/tools.json | 988 ++++++++-------- public/locales/tr/tools.json | 568 ++++----- public/locales/vi/tools.json | 1042 +++++++++-------- public/locales/zh/tools.json | 1036 ++++++++-------- public/sitemap.xml | 6 + src/js/config/tools.ts | 33 +- src/js/i18n/i18n.ts | 144 ++- src/js/logic/email-to-pdf-page.ts | 268 +++++ src/js/logic/email-to-pdf.ts | 393 +++++++ src/js/main.ts | 1 + src/js/types/email-to-pdf-type.ts | 26 + src/js/types/index.ts | 96 +- src/js/utils/helpers.ts | 57 +- .../compress-pdf.html | 864 -------------- .../.backup-related-tools-fix/edit-pdf.html | 634 ---------- .../.backup-related-tools-fix/merge-pdf.html | 754 ------------ .../organize-pdf.html | 576 --------- .../pdf-to-docx.html | 638 ---------- .../.backup-related-tools-fix/pdf-to-jpg.html | 677 ----------- .../.backup-related-tools-fix/rotate-pdf.html | 660 ----------- .../.backup-related-tools-fix/split-pdf.html | 873 -------------- .../word-to-pdf.html | 678 ----------- .../excel-to-pdf.html => email-to-pdf.html} | 232 ++-- vite.config.ts | 5 + 30 files changed, 3974 insertions(+), 9412 deletions(-) create mode 100644 src/js/logic/email-to-pdf-page.ts create mode 100644 src/js/logic/email-to-pdf.ts create mode 100644 src/js/types/email-to-pdf-type.ts delete mode 100644 src/pages/.backup-related-tools-fix/compress-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/edit-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/merge-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/organize-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/pdf-to-docx.html delete mode 100644 src/pages/.backup-related-tools-fix/pdf-to-jpg.html delete mode 100644 src/pages/.backup-related-tools-fix/rotate-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/split-pdf.html delete mode 100644 src/pages/.backup-related-tools-fix/word-to-pdf.html rename src/pages/{.backup-related-tools-fix/excel-to-pdf.html => email-to-pdf.html} (74%) diff --git a/index.html b/index.html index 6f2959f1e..4e6a24487 100644 --- a/index.html +++ b/index.html @@ -95,8 +95,6 @@ /> - - diff --git a/package-lock.json b/package-lock.json index 85740f5b8..849da8cff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-only", "dependencies": { "@bentopdf/gs-wasm": "^0.1.0", - "@bentopdf/pymupdf-wasm": "^0.1.10", + "@bentopdf/pymupdf-wasm": "^0.1.11", "@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/dancing-script": "^5.2.8", "@fontsource/dm-sans": "^5.2.8", @@ -18,6 +18,7 @@ "@fontsource/kalam": "^5.2.8", "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", + "@kenjiuno/msgreader": "^1.27.1-alpha.1", "@matbee/libreoffice-converter": "^2.3.1", "@neslinesli93/qpdf-wasm": "^0.3.0", "@pdf-lib/fontkit": "^1.1.1", @@ -58,6 +59,7 @@ "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.296", "pdfkit": "^0.17.2", + "postal-mime": "^2.7.1", "sortablejs": "^1.15.6", "tailwindcss": "^4.1.14", "terser": "^5.44.0", @@ -514,9 +516,9 @@ } }, "node_modules/@bentopdf/pymupdf-wasm": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@bentopdf/pymupdf-wasm/-/pymupdf-wasm-0.1.10.tgz", - "integrity": "sha512-gej9ItnAswVZhJin8gb0D7rYKqEWvBtO72M4d0eRKP4oARS67eOQERLaKBE4wulIst1x3r3PtHi7673PCmIv+A==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@bentopdf/pymupdf-wasm/-/pymupdf-wasm-0.1.11.tgz", + "integrity": "sha512-sbDFmvm2KzT3oCmqNqMx7w6TMsKpLXeooVK8EVRjyQIV4hU5Ioq0JxWMr8SX7MESu8Caz1feeELd6zt5K966SA==", "license": "AGPL-3.0", "peerDependencies": { "@bentopdf/gs-wasm": "*" @@ -2055,6 +2057,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kenjiuno/decompressrtf": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@kenjiuno/decompressrtf/-/decompressrtf-0.1.4.tgz", + "integrity": "sha512-v9c/iFz17jRWyd2cRnrvJg4VOg/4I/VCk+bG8JnoX2gJ9sAesPzo3uTqcmlVXdpasTI8hChpBVw00pghKe3qTQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@kenjiuno/msgreader": { + "version": "1.27.1-alpha.1", + "resolved": "https://registry.npmjs.org/@kenjiuno/msgreader/-/msgreader-1.27.1-alpha.1.tgz", + "integrity": "sha512-r/Fc6cW+68YpYfA8K0uRI31AV484QzcFzJWZkVz5HHBUf1TrzznvSZ9rRwCRqdO2uTLoMtMf7FovZ+MNfa379g==", + "license": "Apache-2.0", + "dependencies": { + "@kenjiuno/decompressrtf": "^0.1.3", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/@matbee/libreoffice-converter": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@matbee/libreoffice-converter/-/libreoffice-converter-2.3.1.tgz", @@ -9906,6 +9927,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.1.tgz", + "integrity": "sha512-0VslL0CLSV7PBmglwWR8eCGC5fgsdVictjOG4PEA+vvA0+QJF5SC0tV018CbvAcW4XbpbMIJNd91Dt8vTa9kbA==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -9935,9 +9962,9 @@ } }, "node_modules/preact": { - "version": "10.28.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.1.tgz", - "integrity": "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==", + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 3b9ae0a9c..a4d40e7bb 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "@bentopdf/gs-wasm": "^0.1.0", - "@bentopdf/pymupdf-wasm": "^0.1.10", + "@bentopdf/pymupdf-wasm": "^0.1.11", "@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/dancing-script": "^5.2.8", "@fontsource/dm-sans": "^5.2.8", @@ -72,6 +72,7 @@ "@fontsource/kalam": "^5.2.8", "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", + "@kenjiuno/msgreader": "^1.27.1-alpha.1", "@matbee/libreoffice-converter": "^2.3.1", "@neslinesli93/qpdf-wasm": "^0.3.0", "@pdf-lib/fontkit": "^1.1.1", @@ -112,6 +113,7 @@ "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.296", "pdfkit": "^0.17.2", + "postal-mime": "^2.7.1", "sortablejs": "^1.15.6", "tailwindcss": "^4.1.14", "terser": "^5.44.0", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 11f9630d8..d91af6e73 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -1,519 +1,525 @@ { - "categories": { - "popularTools": "Beliebte Werkzeuge", - "editAnnotate": "Bearbeiten & Annotieren", - "convertToPdf": "In PDF konvertieren", - "convertFromPdf": "Aus PDF konvertieren", - "organizeManage": "Organisieren & Verwalten", - "optimizeRepair": "Optimieren & Reparieren", - "securePdf": "PDF sichern" - }, - "pdfMultiTool": { - "name": "PDF Multi-Werkzeug", - "subtitle": "Zusammenführen, Teilen, Organisieren, Löschen, Drehen, Leere Seiten hinzufügen, Extrahieren und Duplizieren in einer einheitlichen Oberfläche." - }, - "mergePdf": { - "name": "PDF zusammenführen", - "subtitle": "Mehrere PDFs zu einer Datei kombinieren. Lesezeichen werden beibehalten." - }, - "splitPdf": { - "name": "PDF teilen", - "subtitle": "Einen Seitenbereich in eine neue PDF extrahieren." - }, - "compressPdf": { - "name": "PDF komprimieren", - "subtitle": "Die Dateigröße Ihrer PDF reduzieren.", - "algorithmLabel": "Komprimierungsalgorithmus", - "condense": "Condense (Empfohlen)", - "photon": "Photon (Für bildlastige PDFs)", - "condenseInfo": "Condense nutzt fortschrittliche Komprimierung: entfernt unnötige Daten, optimiert Bilder, reduziert Schriftarten. Optimal für die meisten PDFs.", - "photonInfo": "Photon wandelt Seiten in Bilder um. Für bildlastige/gescannte PDFs.", - "photonWarning": "Warnung: Text wird nicht mehr auswählbar und Links funktionieren nicht mehr.", - "levelLabel": "Komprimierungsstufe", - "light": "Leicht (Qualität erhalten)", - "balanced": "Ausgewogen (Empfohlen)", - "aggressive": "Aggressiv (Kleinere Dateien)", - "extreme": "Extrem (Maximale Komprimierung)", - "grayscale": "In Graustufen umwandeln", - "grayscaleHint": "Reduziert Dateigröße durch Entfernen von Farbinformationen", - "customSettings": "Erweiterte Einstellungen", - "customSettingsHint": "Komprimierungsparameter anpassen:", - "outputQuality": "Ausgabequalität", - "resizeImagesTo": "Bilder anpassen auf", - "onlyProcessAbove": "Nur verarbeiten über", - "removeMetadata": "Metadaten entfernen", - "subsetFonts": "Schriftarten optimieren (ungenutzte Zeichen entfernen)", - "removeThumbnails": "Eingebettete Miniaturansichten entfernen", - "compressButton": "PDF komprimieren" - }, - "pdfEditor": { - "name": "PDF-Editor", - "subtitle": "Annotieren, hervorheben, schwärzen, kommentieren, Formen/Bilder hinzufügen, suchen und PDFs anzeigen." - }, - "jpgToPdf": { - "name": "JPG zu PDF", - "subtitle": "Eine PDF aus JPG, JPEG und JPEG2000 (JP2/JPX) Bildern erstellen." - }, - "signPdf": { - "name": "PDF unterschreiben", - "subtitle": "Zeichnen, tippen oder laden Sie Ihre Unterschrift hoch." - }, - "cropPdf": { - "name": "PDF zuschneiden", - "subtitle": "Die Ränder jeder Seite in Ihrer PDF beschneiden." - }, - "extractPages": { - "name": "Seiten extrahieren", - "subtitle": "Eine Auswahl von Seiten als neue Dateien speichern." - }, - "duplicateOrganize": { - "name": "Duplizieren & Organisieren", - "subtitle": "Seiten duplizieren, neu anordnen und löschen." - }, - "deletePages": { - "name": "Seiten löschen", - "subtitle": "Bestimmte Seiten aus Ihrem Dokument entfernen." - }, - "editBookmarks": { - "name": "Lesezeichen bearbeiten", - "subtitle": "PDF-Lesezeichen hinzufügen, bearbeiten, importieren, löschen und extrahieren." - }, - "tableOfContents": { - "name": "Inhaltsverzeichnis", - "subtitle": "Ein Inhaltsverzeichnis aus PDF-Lesezeichen generieren." - }, - "pageNumbers": { - "name": "Seitenzahlen", - "subtitle": "Seitenzahlen in Ihr Dokument einfügen." - }, - "addWatermark": { - "name": "Wasserzeichen hinzufügen", - "subtitle": "Text oder ein Bild über Ihre PDF-Seiten stempeln." - }, - "headerFooter": { - "name": "Kopf- & Fußzeile", - "subtitle": "Text oben und unten auf Seiten hinzufügen." - }, - "invertColors": { - "name": "Farben invertieren", - "subtitle": "Eine \"Dunkelmodus\"-Version Ihrer PDF erstellen." - }, - "backgroundColor": { - "name": "Hintergrundfarbe", - "subtitle": "Die Hintergrundfarbe Ihrer PDF ändern." - }, - "changeTextColor": { - "name": "Textfarbe ändern", - "subtitle": "Die Farbe des Textes in Ihrer PDF ändern." - }, - "addStamps": { - "name": "Stempel hinzufügen", - "subtitle": "Bildstempel zu Ihrer PDF über die Annotations-Symbolleiste hinzufügen.", - "usernameLabel": "Stempel-Benutzername", - "usernamePlaceholder": "Geben Sie Ihren Namen ein (für Stempel)", - "usernameHint": "Dieser Name erscheint auf von Ihnen erstellten Stempeln." - }, - "removeAnnotations": { - "name": "Annotationen entfernen", - "subtitle": "Kommentare, Hervorhebungen und Links entfernen." - }, - "pdfFormFiller": { - "name": "PDF-Formular ausfüllen", - "subtitle": "Formulare direkt im Browser ausfüllen. Unterstützt auch XFA-Formulare." - }, - "createPdfForm": { - "name": "PDF-Formular erstellen", - "subtitle": "Ausfüllbare PDF-Formulare mit Drag-and-Drop-Textfeldern erstellen." - }, - "removeBlankPages": { - "name": "Leere Seiten entfernen", - "subtitle": "Leere Seiten automatisch erkennen und löschen." - }, - "imageToPdf": { - "name": "Bilder zu PDF", - "subtitle": "JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP in PDF konvertieren." - }, - "pngToPdf": { - "name": "PNG zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren PNG-Bildern erstellen." - }, - "webpToPdf": { - "name": "WebP zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren WebP-Bildern erstellen." - }, - "svgToPdf": { - "name": "SVG zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren SVG-Bildern erstellen." - }, - "bmpToPdf": { - "name": "BMP zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren BMP-Bildern erstellen." - }, - "heicToPdf": { - "name": "HEIC zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren HEIC-Bildern erstellen." - }, - "tiffToPdf": { - "name": "TIFF zu PDF", - "subtitle": "Eine PDF aus einem oder mehreren TIFF-Bildern erstellen." - }, - "textToPdf": { - "name": "Text zu PDF", - "subtitle": "Eine Textdatei in eine PDF konvertieren." - }, - "jsonToPdf": { - "name": "JSON zu PDF", - "subtitle": "JSON-Dateien in PDF-Format konvertieren." - }, - "pdfToJpg": { - "name": "PDF zu JPG", - "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren." - }, - "pdfToPng": { - "name": "PDF zu PNG", - "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren." - }, - "pdfToWebp": { - "name": "PDF zu WebP", - "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren." - }, - "pdfToBmp": { - "name": "PDF zu BMP", - "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren." - }, - "pdfToTiff": { - "name": "PDF zu TIFF", - "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren." - }, - "pdfToGreyscale": { - "name": "PDF zu Graustufen", - "subtitle": "Alle Farben in Schwarz-Weiß konvertieren." - }, - "pdfToJson": { - "name": "PDF zu JSON", - "subtitle": "PDF-Dateien in JSON-Format konvertieren." - }, - "ocrPdf": { - "name": "OCR PDF", - "subtitle": "Eine PDF durchsuchbar und kopierbar machen." - }, - "alternateMix": { - "name": "Seiten abwechselnd mischen", - "subtitle": "PDFs durch abwechselnde Seiten aus jedem PDF zusammenführen. Lesezeichen werden beibehalten." - }, - "addAttachments": { - "name": "Anhänge hinzufügen", - "subtitle": "Eine oder mehrere Dateien in Ihre PDF einbetten." - }, - "extractAttachments": { - "name": "Anhänge extrahieren", - "subtitle": "Alle eingebetteten Dateien aus PDF(s) als ZIP extrahieren." - }, - "editAttachments": { - "name": "Anhänge bearbeiten", - "subtitle": "Anhänge in Ihrer PDF anzeigen oder entfernen." - }, - "dividePages": { - "name": "Seiten teilen", - "subtitle": "Seiten horizontal oder vertikal teilen." - }, - "addBlankPage": { - "name": "Leere Seite hinzufügen", - "subtitle": "Eine leere Seite an beliebiger Stelle in Ihre PDF einfügen." - }, - "reversePages": { - "name": "Seiten umkehren", - "subtitle": "Die Reihenfolge aller Seiten in Ihrem Dokument umkehren." - }, - "rotatePdf": { - "name": "PDF drehen", - "subtitle": "Seiten in 90-Grad-Schritten drehen." - }, - "rotateCustom": { - "name": "Um benutzerdefinierte Grad drehen", - "subtitle": "Seiten um einen beliebigen Winkel drehen." - }, - "nUpPdf": { - "name": "N-Up PDF", - "subtitle": "Mehrere Seiten auf einem einzigen Blatt anordnen." - }, - "combineToSinglePage": { - "name": "Zu einer Seite kombinieren", - "subtitle": "Alle Seiten zu einem fortlaufenden Dokument zusammenfügen." - }, - "viewMetadata": { - "name": "Metadaten anzeigen", - "subtitle": "Die versteckten Eigenschaften Ihrer PDF inspizieren." - }, - "editMetadata": { - "name": "Metadaten bearbeiten", - "subtitle": "Autor, Titel und andere Eigenschaften ändern." - }, - "pdfsToZip": { - "name": "PDFs zu ZIP", - "subtitle": "Mehrere PDF-Dateien in ein ZIP-Archiv packen." - }, - "comparePdfs": { - "name": "PDFs vergleichen", - "subtitle": "Zwei PDFs nebeneinander vergleichen." - }, - "posterizePdf": { - "name": "PDF posterisieren", - "subtitle": "Eine große Seite in mehrere kleinere Seiten aufteilen." - }, - "fixPageSize": { - "name": "Seitengröße reparieren", - "subtitle": "Alle Seiten auf eine einheitliche Größe standardisieren." - }, - "linearizePdf": { - "name": "PDF linearisieren", - "subtitle": "PDF für schnelle Web-Anzeige optimieren." - }, - "pageDimensions": { - "name": "Seitenmaße", - "subtitle": "Seitengröße, Ausrichtung und Einheiten analysieren." - }, - "removeRestrictions": { - "name": "Beschränkungen entfernen", - "subtitle": "Passwortschutz und Sicherheitsbeschränkungen von digital signierten PDF-Dateien entfernen." - }, - "repairPdf": { - "name": "PDF reparieren", - "subtitle": "Daten aus beschädigten PDF-Dateien wiederherstellen." - }, - "encryptPdf": { - "name": "PDF verschlüsseln", - "subtitle": "Ihre PDF durch Hinzufügen eines Passworts sperren." - }, - "sanitizePdf": { - "name": "PDF bereinigen", - "subtitle": "Metadaten, Annotationen, Skripte und mehr entfernen." - }, - "decryptPdf": { - "name": "PDF entschlüsseln", - "subtitle": "PDF durch Entfernen des Passwortschutzes entsperren." - }, - "flattenPdf": { - "name": "PDF reduzieren", - "subtitle": "Formularfelder und Annotationen nicht editierbar machen." - }, - "removeMetadata": { - "name": "Metadaten entfernen", - "subtitle": "Versteckte Daten aus Ihrer PDF entfernen." - }, - "changePermissions": { - "name": "Berechtigungen ändern", - "subtitle": "Benutzerberechtigungen für eine PDF festlegen oder ändern." - }, - "odtToPdf": { - "name": "ODT zu PDF", - "subtitle": "OpenDocument Text-Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "ODT-Dateien", - "convertButton": "In PDF konvertieren" - }, - "csvToPdf": { - "name": "CSV zu PDF", - "subtitle": "CSV-Tabellendateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "CSV-Dateien", - "convertButton": "In PDF konvertieren" - }, - "rtfToPdf": { - "name": "RTF zu PDF", - "subtitle": "Rich Text Format-Dokumente in PDF konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "RTF-Dateien", - "convertButton": "In PDF konvertieren" - }, - "wordToPdf": { - "name": "Word zu PDF", - "subtitle": "Word-Dokumente (DOCX, DOC, ODT, RTF) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "DOCX, DOC, ODT, RTF-Dateien", - "convertButton": "In PDF konvertieren" - }, - "excelToPdf": { - "name": "Excel zu PDF", - "subtitle": "Excel-Tabellen (XLSX, XLS, ODS, CSV) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "XLSX, XLS, ODS, CSV-Dateien", - "convertButton": "In PDF konvertieren" - }, - "powerpointToPdf": { - "name": "PowerPoint zu PDF", - "subtitle": "PowerPoint-Präsentationen (PPTX, PPT, ODP) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "PPTX, PPT, ODP-Dateien", - "convertButton": "In PDF konvertieren" - }, - "markdownToPdf": { - "name": "Markdown zu PDF", - "subtitle": "Schreiben oder fügen Sie Markdown ein und exportieren Sie es als schön formatiertes PDF.", - "paneMarkdown": "Markdown", - "panePreview": "Vorschau", - "btnUpload": "Hochladen", - "btnSyncScroll": "Sync-Scrollen", - "btnSettings": "Einstellungen", - "btnExportPdf": "PDF exportieren", - "settingsTitle": "Markdown-Einstellungen", - "settingsPreset": "Voreinstellung", - "presetDefault": "Standard (GFM-ähnlich)", - "presetCommonmark": "CommonMark (strikt)", - "presetZero": "Minimal (keine Funktionen)", - "settingsOptions": "Markdown-Optionen", - "optAllowHtml": "HTML-Tags erlauben", - "optBreaks": "Zeilenumbrüche in
umwandeln", - "optLinkify": "URLs automatisch in Links umwandeln", - "optTypographer": "Typograf (intelligente Anführungszeichen usw.)" - }, - "pdfBooklet": { - "name": "PDF-Broschüre", - "subtitle": "Seiten für beidseitigen Broschürendruck neu anordnen. Falten und heften zum Erstellen einer Broschüre.", - "howItWorks": "So funktioniert es:", - "step1": "Eine PDF-Datei hochladen.", - "step2": "Die Seiten werden in Broschürenreihenfolge neu angeordnet.", - "step3": "Beidseitig drucken, an der kurzen Kante wenden, falten und heften.", - "paperSize": "Papiergröße", - "orientation": "Ausrichtung", - "portrait": "Hochformat", - "landscape": "Querformat", - "pagesPerSheet": "Seiten pro Blatt", - "createBooklet": "Broschüre erstellen", - "processing": "Verarbeitung...", - "pageCount": "Die Seitenzahl wird bei Bedarf auf ein Vielfaches von 4 aufgerundet." - }, - "xpsToPdf": { - "name": "XPS zu PDF", - "subtitle": "XPS/OXPS-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "XPS, OXPS-Dateien", - "convertButton": "In PDF konvertieren" - }, - "mobiToPdf": { - "name": "MOBI zu PDF", - "subtitle": "MOBI-E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "MOBI-Dateien", - "convertButton": "In PDF konvertieren" - }, - "epubToPdf": { - "name": "EPUB zu PDF", - "subtitle": "EPUB-E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "EPUB-Dateien", - "convertButton": "In PDF konvertieren" - }, - "fb2ToPdf": { - "name": "FB2 zu PDF", - "subtitle": "FictionBook (FB2) E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "FB2-Dateien", - "convertButton": "In PDF konvertieren" - }, - "cbzToPdf": { - "name": "CBZ zu PDF", - "subtitle": "Comic-Archive (CBZ/CBR) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "CBZ, CBR-Dateien", - "convertButton": "In PDF konvertieren" - }, - "wpdToPdf": { - "name": "WPD zu PDF", - "subtitle": "WordPerfect-Dokumente (WPD) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "WPD-Dateien", - "convertButton": "In PDF konvertieren" - }, - "wpsToPdf": { - "name": "WPS zu PDF", - "subtitle": "WPS Office-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "WPS-Dateien", - "convertButton": "In PDF konvertieren" - }, - "xmlToPdf": { - "name": "XML zu PDF", - "subtitle": "XML-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "XML-Dateien", - "convertButton": "In PDF konvertieren" - }, - "pagesToPdf": { - "name": "Pages zu PDF", - "subtitle": "Apple Pages-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "Pages-Dateien", - "convertButton": "In PDF konvertieren" - }, - "odgToPdf": { - "name": "ODG zu PDF", - "subtitle": "OpenDocument Graphics (ODG) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "ODG-Dateien", - "convertButton": "In PDF konvertieren" - }, - "odsToPdf": { - "name": "ODS zu PDF", - "subtitle": "OpenDocument Spreadsheet (ODS) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "ODS-Dateien", - "convertButton": "In PDF konvertieren" - }, - "odpToPdf": { - "name": "ODP zu PDF", - "subtitle": "OpenDocument Presentation (ODP) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "ODP-Dateien", - "convertButton": "In PDF konvertieren" - }, - "pubToPdf": { - "name": "PUB zu PDF", - "subtitle": "Microsoft Publisher (PUB) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "PUB-Dateien", - "convertButton": "In PDF konvertieren" - }, - "vsdToPdf": { - "name": "VSD zu PDF", - "subtitle": "Microsoft Visio (VSD, VSDX) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "VSD, VSDX-Dateien", - "convertButton": "In PDF konvertieren" - }, - "psdToPdf": { - "name": "PSD zu PDF", - "subtitle": "Adobe Photoshop (PSD) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", - "acceptedFormats": "PSD-Dateien", - "convertButton": "In PDF konvertieren" - }, - "pdfToSvg": { - "name": "PDF zu SVG", - "subtitle": "Jede Seite einer PDF-Datei in eine skalierbare Vektorgrafik (SVG) konvertieren für perfekte Qualität in jeder Größe." - }, - "extractTables": { - "name": "PDF-Tabellen extrahieren", - "subtitle": "Tabellen aus PDF-Dateien extrahieren und als CSV, JSON oder Markdown exportieren." - }, - "pdfToCsv": { - "name": "PDF zu CSV", - "subtitle": "Tabellen aus PDF extrahieren und in CSV-Format konvertieren." - }, - "pdfToExcel": { - "name": "PDF zu Excel", - "subtitle": "Tabellen aus PDF extrahieren und in Excel (XLSX) Format konvertieren." - }, - "pdfToText": { - "name": "PDF zu Text", - "subtitle": "Text aus PDF-Dateien extrahieren und als Textdatei (.txt) speichern. Unterstützt mehrere Dateien.", - "note": "Dieses Tool funktioniert NUR mit digital erstellten PDFs. Für gescannte Dokumente oder bildbasierte PDFs verwenden Sie stattdessen unser OCR PDF-Tool.", - "convertButton": "Text extrahieren" - }, - "digitalSignPdf": { - "name": "Digitale PDF-Signatur", - "pageTitle": "Digitale PDF-Signatur - Kryptografische Signatur hinzufügen | BentoPDF", - "subtitle": "Fügen Sie eine kryptografische digitale Signatur mit X.509-Zertifikaten zu Ihrer PDF hinzu. Unterstützt PKCS#12 (.pfx, .p12) und PEM-Formate. Ihr privater Schlüssel verlässt niemals Ihren Browser.", - "certificateSection": "Zertifikat", - "uploadCert": "Zertifikat hochladen (.pfx, .p12)", - "certPassword": "Zertifikat-Passwort", - "certPasswordPlaceholder": "Zertifikat-Passwort eingeben", - "certInfo": "Zertifikat-Informationen", - "certSubject": "Betreff", - "certIssuer": "Aussteller", - "certValidity": "Gültig", - "signatureDetails": "Signatur-Details (Optional)", - "reason": "Grund", - "reasonPlaceholder": "z.B. Ich genehmige dieses Dokument", - "location": "Ort", - "locationPlaceholder": "z.B. Berlin, Deutschland", - "contactInfo": "Kontaktdaten", - "contactPlaceholder": "z.B. email@beispiel.de", - "applySignature": "Digitale Signatur anwenden", - "successMessage": "PDF erfolgreich signiert! Die Signatur kann in jedem PDF-Reader überprüft werden." - }, - "validateSignaturePdf": { - "name": "PDF-Signatur überprüfen", - "pageTitle": "PDF-Signatur überprüfen - Digitale Signaturen verifizieren | BentoPDF", - "subtitle": "Überprüfen Sie digitale Signaturen in Ihren PDF-Dateien. Prüfen Sie die Zertifikatsgültigkeit, sehen Sie Unterzeichnerdetails und bestätigen Sie die Dokumentenintegrität. Die gesamte Verarbeitung erfolgt in Ihrem Browser." - } -} \ No newline at end of file + "categories": { + "popularTools": "Beliebte Werkzeuge", + "editAnnotate": "Bearbeiten & Annotieren", + "convertToPdf": "In PDF konvertieren", + "convertFromPdf": "Aus PDF konvertieren", + "organizeManage": "Organisieren & Verwalten", + "optimizeRepair": "Optimieren & Reparieren", + "securePdf": "PDF sichern" + }, + "pdfMultiTool": { + "name": "PDF Multi-Werkzeug", + "subtitle": "Zusammenführen, Teilen, Organisieren, Löschen, Drehen, Leere Seiten hinzufügen, Extrahieren und Duplizieren in einer einheitlichen Oberfläche." + }, + "mergePdf": { + "name": "PDF zusammenführen", + "subtitle": "Mehrere PDFs zu einer Datei kombinieren. Lesezeichen werden beibehalten." + }, + "splitPdf": { + "name": "PDF teilen", + "subtitle": "Einen Seitenbereich in eine neue PDF extrahieren." + }, + "compressPdf": { + "name": "PDF komprimieren", + "subtitle": "Die Dateigröße Ihrer PDF reduzieren.", + "algorithmLabel": "Komprimierungsalgorithmus", + "condense": "Condense (Empfohlen)", + "photon": "Photon (Für bildlastige PDFs)", + "condenseInfo": "Condense nutzt fortschrittliche Komprimierung: entfernt unnötige Daten, optimiert Bilder, reduziert Schriftarten. Optimal für die meisten PDFs.", + "photonInfo": "Photon wandelt Seiten in Bilder um. Für bildlastige/gescannte PDFs.", + "photonWarning": "Warnung: Text wird nicht mehr auswählbar und Links funktionieren nicht mehr.", + "levelLabel": "Komprimierungsstufe", + "light": "Leicht (Qualität erhalten)", + "balanced": "Ausgewogen (Empfohlen)", + "aggressive": "Aggressiv (Kleinere Dateien)", + "extreme": "Extrem (Maximale Komprimierung)", + "grayscale": "In Graustufen umwandeln", + "grayscaleHint": "Reduziert Dateigröße durch Entfernen von Farbinformationen", + "customSettings": "Erweiterte Einstellungen", + "customSettingsHint": "Komprimierungsparameter anpassen:", + "outputQuality": "Ausgabequalität", + "resizeImagesTo": "Bilder anpassen auf", + "onlyProcessAbove": "Nur verarbeiten über", + "removeMetadata": "Metadaten entfernen", + "subsetFonts": "Schriftarten optimieren (ungenutzte Zeichen entfernen)", + "removeThumbnails": "Eingebettete Miniaturansichten entfernen", + "compressButton": "PDF komprimieren" + }, + "pdfEditor": { + "name": "PDF-Editor", + "subtitle": "Annotieren, hervorheben, schwärzen, kommentieren, Formen/Bilder hinzufügen, suchen und PDFs anzeigen." + }, + "jpgToPdf": { + "name": "JPG zu PDF", + "subtitle": "Eine PDF aus JPG, JPEG und JPEG2000 (JP2/JPX) Bildern erstellen." + }, + "signPdf": { + "name": "PDF unterschreiben", + "subtitle": "Zeichnen, tippen oder laden Sie Ihre Unterschrift hoch." + }, + "cropPdf": { + "name": "PDF zuschneiden", + "subtitle": "Die Ränder jeder Seite in Ihrer PDF beschneiden." + }, + "extractPages": { + "name": "Seiten extrahieren", + "subtitle": "Eine Auswahl von Seiten als neue Dateien speichern." + }, + "duplicateOrganize": { + "name": "Duplizieren & Organisieren", + "subtitle": "Seiten duplizieren, neu anordnen und löschen." + }, + "deletePages": { + "name": "Seiten löschen", + "subtitle": "Bestimmte Seiten aus Ihrem Dokument entfernen." + }, + "editBookmarks": { + "name": "Lesezeichen bearbeiten", + "subtitle": "PDF-Lesezeichen hinzufügen, bearbeiten, importieren, löschen und extrahieren." + }, + "tableOfContents": { + "name": "Inhaltsverzeichnis", + "subtitle": "Ein Inhaltsverzeichnis aus PDF-Lesezeichen generieren." + }, + "pageNumbers": { + "name": "Seitenzahlen", + "subtitle": "Seitenzahlen in Ihr Dokument einfügen." + }, + "addWatermark": { + "name": "Wasserzeichen hinzufügen", + "subtitle": "Text oder ein Bild über Ihre PDF-Seiten stempeln." + }, + "headerFooter": { + "name": "Kopf- & Fußzeile", + "subtitle": "Text oben und unten auf Seiten hinzufügen." + }, + "invertColors": { + "name": "Farben invertieren", + "subtitle": "Eine \"Dunkelmodus\"-Version Ihrer PDF erstellen." + }, + "backgroundColor": { + "name": "Hintergrundfarbe", + "subtitle": "Die Hintergrundfarbe Ihrer PDF ändern." + }, + "changeTextColor": { + "name": "Textfarbe ändern", + "subtitle": "Die Farbe des Textes in Ihrer PDF ändern." + }, + "addStamps": { + "name": "Stempel hinzufügen", + "subtitle": "Bildstempel zu Ihrer PDF über die Annotations-Symbolleiste hinzufügen.", + "usernameLabel": "Stempel-Benutzername", + "usernamePlaceholder": "Geben Sie Ihren Namen ein (für Stempel)", + "usernameHint": "Dieser Name erscheint auf von Ihnen erstellten Stempeln." + }, + "removeAnnotations": { + "name": "Annotationen entfernen", + "subtitle": "Kommentare, Hervorhebungen und Links entfernen." + }, + "pdfFormFiller": { + "name": "PDF-Formular ausfüllen", + "subtitle": "Formulare direkt im Browser ausfüllen. Unterstützt auch XFA-Formulare." + }, + "createPdfForm": { + "name": "PDF-Formular erstellen", + "subtitle": "Ausfüllbare PDF-Formulare mit Drag-and-Drop-Textfeldern erstellen." + }, + "removeBlankPages": { + "name": "Leere Seiten entfernen", + "subtitle": "Leere Seiten automatisch erkennen und löschen." + }, + "imageToPdf": { + "name": "Bilder zu PDF", + "subtitle": "JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP in PDF konvertieren." + }, + "pngToPdf": { + "name": "PNG zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren PNG-Bildern erstellen." + }, + "webpToPdf": { + "name": "WebP zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren WebP-Bildern erstellen." + }, + "svgToPdf": { + "name": "SVG zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren SVG-Bildern erstellen." + }, + "bmpToPdf": { + "name": "BMP zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren BMP-Bildern erstellen." + }, + "heicToPdf": { + "name": "HEIC zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren HEIC-Bildern erstellen." + }, + "tiffToPdf": { + "name": "TIFF zu PDF", + "subtitle": "Eine PDF aus einem oder mehreren TIFF-Bildern erstellen." + }, + "textToPdf": { + "name": "Text zu PDF", + "subtitle": "Eine Textdatei in eine PDF konvertieren." + }, + "jsonToPdf": { + "name": "JSON zu PDF", + "subtitle": "JSON-Dateien in PDF-Format konvertieren." + }, + "pdfToJpg": { + "name": "PDF zu JPG", + "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren." + }, + "pdfToPng": { + "name": "PDF zu PNG", + "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren." + }, + "pdfToWebp": { + "name": "PDF zu WebP", + "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren." + }, + "pdfToBmp": { + "name": "PDF zu BMP", + "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren." + }, + "pdfToTiff": { + "name": "PDF zu TIFF", + "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren." + }, + "pdfToGreyscale": { + "name": "PDF zu Graustufen", + "subtitle": "Alle Farben in Schwarz-Weiß konvertieren." + }, + "pdfToJson": { + "name": "PDF zu JSON", + "subtitle": "PDF-Dateien in JSON-Format konvertieren." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Eine PDF durchsuchbar und kopierbar machen." + }, + "alternateMix": { + "name": "Seiten abwechselnd mischen", + "subtitle": "PDFs durch abwechselnde Seiten aus jedem PDF zusammenführen. Lesezeichen werden beibehalten." + }, + "addAttachments": { + "name": "Anhänge hinzufügen", + "subtitle": "Eine oder mehrere Dateien in Ihre PDF einbetten." + }, + "extractAttachments": { + "name": "Anhänge extrahieren", + "subtitle": "Alle eingebetteten Dateien aus PDF(s) als ZIP extrahieren." + }, + "editAttachments": { + "name": "Anhänge bearbeiten", + "subtitle": "Anhänge in Ihrer PDF anzeigen oder entfernen." + }, + "dividePages": { + "name": "Seiten teilen", + "subtitle": "Seiten horizontal oder vertikal teilen." + }, + "addBlankPage": { + "name": "Leere Seite hinzufügen", + "subtitle": "Eine leere Seite an beliebiger Stelle in Ihre PDF einfügen." + }, + "reversePages": { + "name": "Seiten umkehren", + "subtitle": "Die Reihenfolge aller Seiten in Ihrem Dokument umkehren." + }, + "rotatePdf": { + "name": "PDF drehen", + "subtitle": "Seiten in 90-Grad-Schritten drehen." + }, + "rotateCustom": { + "name": "Um benutzerdefinierte Grad drehen", + "subtitle": "Seiten um einen beliebigen Winkel drehen." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Mehrere Seiten auf einem einzigen Blatt anordnen." + }, + "combineToSinglePage": { + "name": "Zu einer Seite kombinieren", + "subtitle": "Alle Seiten zu einem fortlaufenden Dokument zusammenfügen." + }, + "viewMetadata": { + "name": "Metadaten anzeigen", + "subtitle": "Die versteckten Eigenschaften Ihrer PDF inspizieren." + }, + "editMetadata": { + "name": "Metadaten bearbeiten", + "subtitle": "Autor, Titel und andere Eigenschaften ändern." + }, + "pdfsToZip": { + "name": "PDFs zu ZIP", + "subtitle": "Mehrere PDF-Dateien in ein ZIP-Archiv packen." + }, + "comparePdfs": { + "name": "PDFs vergleichen", + "subtitle": "Zwei PDFs nebeneinander vergleichen." + }, + "posterizePdf": { + "name": "PDF posterisieren", + "subtitle": "Eine große Seite in mehrere kleinere Seiten aufteilen." + }, + "fixPageSize": { + "name": "Seitengröße reparieren", + "subtitle": "Alle Seiten auf eine einheitliche Größe standardisieren." + }, + "linearizePdf": { + "name": "PDF linearisieren", + "subtitle": "PDF für schnelle Web-Anzeige optimieren." + }, + "pageDimensions": { + "name": "Seitenmaße", + "subtitle": "Seitengröße, Ausrichtung und Einheiten analysieren." + }, + "removeRestrictions": { + "name": "Beschränkungen entfernen", + "subtitle": "Passwortschutz und Sicherheitsbeschränkungen von digital signierten PDF-Dateien entfernen." + }, + "repairPdf": { + "name": "PDF reparieren", + "subtitle": "Daten aus beschädigten PDF-Dateien wiederherstellen." + }, + "encryptPdf": { + "name": "PDF verschlüsseln", + "subtitle": "Ihre PDF durch Hinzufügen eines Passworts sperren." + }, + "sanitizePdf": { + "name": "PDF bereinigen", + "subtitle": "Metadaten, Annotationen, Skripte und mehr entfernen." + }, + "decryptPdf": { + "name": "PDF entschlüsseln", + "subtitle": "PDF durch Entfernen des Passwortschutzes entsperren." + }, + "flattenPdf": { + "name": "PDF reduzieren", + "subtitle": "Formularfelder und Annotationen nicht editierbar machen." + }, + "removeMetadata": { + "name": "Metadaten entfernen", + "subtitle": "Versteckte Daten aus Ihrer PDF entfernen." + }, + "changePermissions": { + "name": "Berechtigungen ändern", + "subtitle": "Benutzerberechtigungen für eine PDF festlegen oder ändern." + }, + "odtToPdf": { + "name": "ODT zu PDF", + "subtitle": "OpenDocument Text-Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "ODT-Dateien", + "convertButton": "In PDF konvertieren" + }, + "csvToPdf": { + "name": "CSV zu PDF", + "subtitle": "CSV-Tabellendateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "CSV-Dateien", + "convertButton": "In PDF konvertieren" + }, + "rtfToPdf": { + "name": "RTF zu PDF", + "subtitle": "Rich Text Format-Dokumente in PDF konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "RTF-Dateien", + "convertButton": "In PDF konvertieren" + }, + "wordToPdf": { + "name": "Word zu PDF", + "subtitle": "Word-Dokumente (DOCX, DOC, ODT, RTF) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "DOCX, DOC, ODT, RTF-Dateien", + "convertButton": "In PDF konvertieren" + }, + "excelToPdf": { + "name": "Excel zu PDF", + "subtitle": "Excel-Tabellen (XLSX, XLS, ODS, CSV) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "XLSX, XLS, ODS, CSV-Dateien", + "convertButton": "In PDF konvertieren" + }, + "powerpointToPdf": { + "name": "PowerPoint zu PDF", + "subtitle": "PowerPoint-Präsentationen (PPTX, PPT, ODP) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "PPTX, PPT, ODP-Dateien", + "convertButton": "In PDF konvertieren" + }, + "markdownToPdf": { + "name": "Markdown zu PDF", + "subtitle": "Schreiben oder fügen Sie Markdown ein und exportieren Sie es als schön formatiertes PDF.", + "paneMarkdown": "Markdown", + "panePreview": "Vorschau", + "btnUpload": "Hochladen", + "btnSyncScroll": "Sync-Scrollen", + "btnSettings": "Einstellungen", + "btnExportPdf": "PDF exportieren", + "settingsTitle": "Markdown-Einstellungen", + "settingsPreset": "Voreinstellung", + "presetDefault": "Standard (GFM-ähnlich)", + "presetCommonmark": "CommonMark (strikt)", + "presetZero": "Minimal (keine Funktionen)", + "settingsOptions": "Markdown-Optionen", + "optAllowHtml": "HTML-Tags erlauben", + "optBreaks": "Zeilenumbrüche in
umwandeln", + "optLinkify": "URLs automatisch in Links umwandeln", + "optTypographer": "Typograf (intelligente Anführungszeichen usw.)" + }, + "pdfBooklet": { + "name": "PDF-Broschüre", + "subtitle": "Seiten für beidseitigen Broschürendruck neu anordnen. Falten und heften zum Erstellen einer Broschüre.", + "howItWorks": "So funktioniert es:", + "step1": "Eine PDF-Datei hochladen.", + "step2": "Die Seiten werden in Broschürenreihenfolge neu angeordnet.", + "step3": "Beidseitig drucken, an der kurzen Kante wenden, falten und heften.", + "paperSize": "Papiergröße", + "orientation": "Ausrichtung", + "portrait": "Hochformat", + "landscape": "Querformat", + "pagesPerSheet": "Seiten pro Blatt", + "createBooklet": "Broschüre erstellen", + "processing": "Verarbeitung...", + "pageCount": "Die Seitenzahl wird bei Bedarf auf ein Vielfaches von 4 aufgerundet." + }, + "xpsToPdf": { + "name": "XPS zu PDF", + "subtitle": "XPS/OXPS-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "XPS, OXPS-Dateien", + "convertButton": "In PDF konvertieren" + }, + "mobiToPdf": { + "name": "MOBI zu PDF", + "subtitle": "MOBI-E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "MOBI-Dateien", + "convertButton": "In PDF konvertieren" + }, + "epubToPdf": { + "name": "EPUB zu PDF", + "subtitle": "EPUB-E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "EPUB-Dateien", + "convertButton": "In PDF konvertieren" + }, + "fb2ToPdf": { + "name": "FB2 zu PDF", + "subtitle": "FictionBook (FB2) E-Books in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "FB2-Dateien", + "convertButton": "In PDF konvertieren" + }, + "cbzToPdf": { + "name": "CBZ zu PDF", + "subtitle": "Comic-Archive (CBZ/CBR) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "CBZ, CBR-Dateien", + "convertButton": "In PDF konvertieren" + }, + "wpdToPdf": { + "name": "WPD zu PDF", + "subtitle": "WordPerfect-Dokumente (WPD) in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "WPD-Dateien", + "convertButton": "In PDF konvertieren" + }, + "wpsToPdf": { + "name": "WPS zu PDF", + "subtitle": "WPS Office-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "WPS-Dateien", + "convertButton": "In PDF konvertieren" + }, + "xmlToPdf": { + "name": "XML zu PDF", + "subtitle": "XML-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "XML-Dateien", + "convertButton": "In PDF konvertieren" + }, + "pagesToPdf": { + "name": "Pages zu PDF", + "subtitle": "Apple Pages-Dokumente in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "Pages-Dateien", + "convertButton": "In PDF konvertieren" + }, + "odgToPdf": { + "name": "ODG zu PDF", + "subtitle": "OpenDocument Graphics (ODG) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "ODG-Dateien", + "convertButton": "In PDF konvertieren" + }, + "odsToPdf": { + "name": "ODS zu PDF", + "subtitle": "OpenDocument Spreadsheet (ODS) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "ODS-Dateien", + "convertButton": "In PDF konvertieren" + }, + "odpToPdf": { + "name": "ODP zu PDF", + "subtitle": "OpenDocument Presentation (ODP) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "ODP-Dateien", + "convertButton": "In PDF konvertieren" + }, + "pubToPdf": { + "name": "PUB zu PDF", + "subtitle": "Microsoft Publisher (PUB) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "PUB-Dateien", + "convertButton": "In PDF konvertieren" + }, + "vsdToPdf": { + "name": "VSD zu PDF", + "subtitle": "Microsoft Visio (VSD, VSDX) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "VSD, VSDX-Dateien", + "convertButton": "In PDF konvertieren" + }, + "psdToPdf": { + "name": "PSD zu PDF", + "subtitle": "Adobe Photoshop (PSD) Dateien in PDF-Format konvertieren. Unterstützt mehrere Dateien.", + "acceptedFormats": "PSD-Dateien", + "convertButton": "In PDF konvertieren" + }, + "pdfToSvg": { + "name": "PDF zu SVG", + "subtitle": "Jede Seite einer PDF-Datei in eine skalierbare Vektorgrafik (SVG) konvertieren für perfekte Qualität in jeder Größe." + }, + "extractTables": { + "name": "PDF-Tabellen extrahieren", + "subtitle": "Tabellen aus PDF-Dateien extrahieren und als CSV, JSON oder Markdown exportieren." + }, + "pdfToCsv": { + "name": "PDF zu CSV", + "subtitle": "Tabellen aus PDF extrahieren und in CSV-Format konvertieren." + }, + "pdfToExcel": { + "name": "PDF zu Excel", + "subtitle": "Tabellen aus PDF extrahieren und in Excel (XLSX) Format konvertieren." + }, + "pdfToText": { + "name": "PDF zu Text", + "subtitle": "Text aus PDF-Dateien extrahieren und als Textdatei (.txt) speichern. Unterstützt mehrere Dateien.", + "note": "Dieses Tool funktioniert NUR mit digital erstellten PDFs. Für gescannte Dokumente oder bildbasierte PDFs verwenden Sie stattdessen unser OCR PDF-Tool.", + "convertButton": "Text extrahieren" + }, + "digitalSignPdf": { + "name": "Digitale PDF-Signatur", + "pageTitle": "Digitale PDF-Signatur - Kryptografische Signatur hinzufügen | BentoPDF", + "subtitle": "Fügen Sie eine kryptografische digitale Signatur mit X.509-Zertifikaten zu Ihrer PDF hinzu. Unterstützt PKCS#12 (.pfx, .p12) und PEM-Formate. Ihr privater Schlüssel verlässt niemals Ihren Browser.", + "certificateSection": "Zertifikat", + "uploadCert": "Zertifikat hochladen (.pfx, .p12)", + "certPassword": "Zertifikat-Passwort", + "certPasswordPlaceholder": "Zertifikat-Passwort eingeben", + "certInfo": "Zertifikat-Informationen", + "certSubject": "Betreff", + "certIssuer": "Aussteller", + "certValidity": "Gültig", + "signatureDetails": "Signatur-Details (Optional)", + "reason": "Grund", + "reasonPlaceholder": "z.B. Ich genehmige dieses Dokument", + "location": "Ort", + "locationPlaceholder": "z.B. Berlin, Deutschland", + "contactInfo": "Kontaktdaten", + "contactPlaceholder": "z.B. email@beispiel.de", + "applySignature": "Digitale Signatur anwenden", + "successMessage": "PDF erfolgreich signiert! Die Signatur kann in jedem PDF-Reader überprüft werden." + }, + "validateSignaturePdf": { + "name": "PDF-Signatur überprüfen", + "pageTitle": "PDF-Signatur überprüfen - Digitale Signaturen verifizieren | BentoPDF", + "subtitle": "Überprüfen Sie digitale Signaturen in Ihren PDF-Dateien. Prüfen Sie die Zertifikatsgültigkeit, sehen Sie Unterzeichnerdetails und bestätigen Sie die Dokumentenintegrität. Die gesamte Verarbeitung erfolgt in Ihrem Browser." + }, + "emailToPdf": { + "name": "E-Mail zu PDF", + "subtitle": "E-Mail-Dateien (EML, MSG) in PDF-Format konvertieren. Unterstützt Outlook-Exporte und Standard-E-Mail-Formate.", + "acceptedFormats": "EML, MSG-Dateien", + "convertButton": "In PDF konvertieren" + } +} diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index c481e60bf..a85839ca1 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -1,519 +1,525 @@ { - "categories": { - "popularTools": "Popular Tools", - "editAnnotate": "Edit & Annotate", - "convertToPdf": "Convert to PDF", - "convertFromPdf": "Convert from PDF", - "organizeManage": "Organize & Manage", - "optimizeRepair": "Optimize & Repair", - "securePdf": "Secure PDF" - }, - "pdfMultiTool": { - "name": "PDF Multi Tool", - "subtitle": "Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface." - }, - "mergePdf": { - "name": "Merge PDF", - "subtitle": "Combine multiple PDFs into one file. Preserves Bookmarks." - }, - "splitPdf": { - "name": "Split PDF", - "subtitle": "Extract a range of pages into a new PDF." - }, - "compressPdf": { - "name": "Compress PDF", - "subtitle": "Reduce the file size of your PDF.", - "algorithmLabel": "Compression Algorithm", - "condense": "Condense (Recommended)", - "photon": "Photon (For Photo-Heavy PDFs)", - "condenseInfo": "Condense uses advanced compression: removes dead-weight, optimizes images, subsets fonts. Best for most PDFs.", - "photonInfo": "Photon converts pages to images. Use for photo-heavy/scanned PDFs.", - "photonWarning": "Warning: Text will become non-selectable and links will stop working.", - "levelLabel": "Compression Level", - "light": "Light (Preserve Quality)", - "balanced": "Balanced (Recommended)", - "aggressive": "Aggressive (Smaller Files)", - "extreme": "Extreme (Maximum Compression)", - "grayscale": "Convert to Grayscale", - "grayscaleHint": "Reduces file size by removing color information", - "customSettings": "Custom Settings", - "customSettingsHint": "Fine-tune compression parameters:", - "outputQuality": "Output Quality", - "resizeImagesTo": "Resize Images To", - "onlyProcessAbove": "Only Process Above", - "removeMetadata": "Remove metadata", - "subsetFonts": "Subset fonts (remove unused glyphs)", - "removeThumbnails": "Remove embedded thumbnails", - "compressButton": "Compress PDF" - }, - "pdfEditor": { - "name": "PDF Editor", - "subtitle": "Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs." - }, - "jpgToPdf": { - "name": "JPG to PDF", - "subtitle": "Create a PDF from JPG, JPEG, and JPEG2000 (JP2/JPX) images." - }, - "signPdf": { - "name": "Sign PDF", - "subtitle": "Draw, type, or upload your signature." - }, - "cropPdf": { - "name": "Crop PDF", - "subtitle": "Trim the margins of every page in your PDF." - }, - "extractPages": { - "name": "Extract Pages", - "subtitle": "Save a selection of pages as new files." - }, - "duplicateOrganize": { - "name": "Duplicate & Organize", - "subtitle": "Duplicate, reorder, and delete pages." - }, - "deletePages": { - "name": "Delete Pages", - "subtitle": "Remove specific pages from your document." - }, - "editBookmarks": { - "name": "Edit Bookmarks", - "subtitle": "Add, edit, import, delete and extract PDF bookmarks." - }, - "tableOfContents": { - "name": "Table of Contents", - "subtitle": "Generate a table of contents page from PDF bookmarks." - }, - "pageNumbers": { - "name": "Page Numbers", - "subtitle": "Insert page numbers into your document." - }, - "addWatermark": { - "name": "Add Watermark", - "subtitle": "Stamp text or an image over your PDF pages." - }, - "headerFooter": { - "name": "Header & Footer", - "subtitle": "Add text to the top and bottom of pages." - }, - "invertColors": { - "name": "Invert Colors", - "subtitle": "Create a \"dark mode\" version of your PDF." - }, - "backgroundColor": { - "name": "Background Color", - "subtitle": "Change the background color of your PDF." - }, - "changeTextColor": { - "name": "Change Text Color", - "subtitle": "Change the color of text in your PDF." - }, - "addStamps": { - "name": "Add Stamps", - "subtitle": "Add image stamps to your PDF using the annotation toolbar.", - "usernameLabel": "Stamp Username", - "usernamePlaceholder": "Enter your name (for stamps)", - "usernameHint": "This name will appear on stamps you create." - }, - "removeAnnotations": { - "name": "Remove Annotations", - "subtitle": "Strip comments, highlights, and links." - }, - "pdfFormFiller": { - "name": "PDF Form Filler", - "subtitle": "Fill in forms directly in the browser. Also supports XFA forms." - }, - "createPdfForm": { - "name": "Create PDF Form", - "subtitle": "Create fillable PDF forms with drag-and-drop text fields." - }, - "removeBlankPages": { - "name": "Remove Blank Pages", - "subtitle": "Automatically detect and delete blank pages." - }, - "imageToPdf": { - "name": "Images to PDF", - "subtitle": "Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP to PDF." - }, - "pngToPdf": { - "name": "PNG to PDF", - "subtitle": "Create a PDF from one or more PNG images." - }, - "webpToPdf": { - "name": "WebP to PDF", - "subtitle": "Create a PDF from one or more WebP images." - }, - "svgToPdf": { - "name": "SVG to PDF", - "subtitle": "Create a PDF from one or more SVG images." - }, - "bmpToPdf": { - "name": "BMP to PDF", - "subtitle": "Create a PDF from one or more BMP images." - }, - "heicToPdf": { - "name": "HEIC to PDF", - "subtitle": "Create a PDF from one or more HEIC images." - }, - "tiffToPdf": { - "name": "TIFF to PDF", - "subtitle": "Create a PDF from one or more TIFF images." - }, - "textToPdf": { - "name": "Text to PDF", - "subtitle": "Convert a plain text file into a PDF." - }, - "jsonToPdf": { - "name": "JSON to PDF", - "subtitle": "Convert JSON files to PDF format." - }, - "pdfToJpg": { - "name": "PDF to JPG", - "subtitle": "Convert each PDF page into a JPG image." - }, - "pdfToPng": { - "name": "PDF to PNG", - "subtitle": "Convert each PDF page into a PNG image." - }, - "pdfToWebp": { - "name": "PDF to WebP", - "subtitle": "Convert each PDF page into a WebP image." - }, - "pdfToBmp": { - "name": "PDF to BMP", - "subtitle": "Convert each PDF page into a BMP image." - }, - "pdfToTiff": { - "name": "PDF to TIFF", - "subtitle": "Convert each PDF page into a TIFF image." - }, - "pdfToGreyscale": { - "name": "PDF to Greyscale", - "subtitle": "Convert all colors to black and white." - }, - "pdfToJson": { - "name": "PDF to JSON", - "subtitle": "Convert PDF files to JSON format." - }, - "ocrPdf": { - "name": "OCR PDF", - "subtitle": "Make a PDF searchable and copyable." - }, - "alternateMix": { - "name": "Alternate & Mix Pages", - "subtitle": "Merge PDFs by alternating pages from each PDF. Preserves Bookmarks." - }, - "addAttachments": { - "name": "Add Attachments", - "subtitle": "Embed one or more files into your PDF." - }, - "extractAttachments": { - "name": "Extract Attachments", - "subtitle": "Extract all embedded files from PDF(s) as a ZIP." - }, - "editAttachments": { - "name": "Edit Attachments", - "subtitle": "View or remove attachments in your PDF." - }, - "dividePages": { - "name": "Divide Pages", - "subtitle": "Divide pages horizontally or vertically." - }, - "addBlankPage": { - "name": "Add Blank Page", - "subtitle": "Insert an empty page anywhere in your PDF." - }, - "reversePages": { - "name": "Reverse Pages", - "subtitle": "Flip the order of all pages in your document." - }, - "rotatePdf": { - "name": "Rotate PDF", - "subtitle": "Turn pages in 90-degree increments." - }, - "rotateCustom": { - "name": "Rotate by Custom Degrees", - "subtitle": "Rotate pages by any custom angle." - }, - "nUpPdf": { - "name": "N-Up PDF", - "subtitle": "Arrange multiple pages onto a single sheet." - }, - "combineToSinglePage": { - "name": "Combine to Single Page", - "subtitle": "Stitch all pages into one continuous scroll." - }, - "viewMetadata": { - "name": "View Metadata", - "subtitle": "Inspect the hidden properties of your PDF." - }, - "editMetadata": { - "name": "Edit Metadata", - "subtitle": "Change the author, title, and other properties." - }, - "pdfsToZip": { - "name": "PDFs to ZIP", - "subtitle": "Package multiple PDF files into a ZIP archive." - }, - "comparePdfs": { - "name": "Compare PDFs", - "subtitle": "Compare two PDFs side by side." - }, - "posterizePdf": { - "name": "Posterize PDF", - "subtitle": "Split a large page into multiple smaller pages." - }, - "fixPageSize": { - "name": "Fix Page Size", - "subtitle": "Standardize all pages to a uniform size." - }, - "linearizePdf": { - "name": "Linearize PDF", - "subtitle": "Optimize PDF for fast web viewing." - }, - "pageDimensions": { - "name": "Page Dimensions", - "subtitle": "Analyze page size, orientation, and units." - }, - "removeRestrictions": { - "name": "Remove Restrictions", - "subtitle": "Remove password protection and security restrictions associated with digitally signed PDF files." - }, - "repairPdf": { - "name": "Repair PDF", - "subtitle": "Recover data from corrupted or damaged PDF files." - }, - "encryptPdf": { - "name": "Encrypt PDF", - "subtitle": "Lock your PDF by adding a password." - }, - "sanitizePdf": { - "name": "Sanitize PDF", - "subtitle": "Remove metadata, annotations, scripts, and more." - }, - "decryptPdf": { - "name": "Decrypt PDF", - "subtitle": "Unlock PDF by removing password protection." - }, - "flattenPdf": { - "name": "Flatten PDF", - "subtitle": "Make form fields and annotations non-editable." - }, - "removeMetadata": { - "name": "Remove Metadata", - "subtitle": "Strip hidden data from your PDF." - }, - "changePermissions": { - "name": "Change Permissions", - "subtitle": "Set or change user permissions on a PDF." - }, - "odtToPdf": { - "name": "ODT to PDF", - "subtitle": "Convert OpenDocument Text files to PDF format. Supports multiple files.", - "acceptedFormats": "ODT files", - "convertButton": "Convert to PDF" - }, - "csvToPdf": { - "name": "CSV to PDF", - "subtitle": "Convert CSV spreadsheet files to PDF format. Supports multiple files.", - "acceptedFormats": "CSV files", - "convertButton": "Convert to PDF" - }, - "rtfToPdf": { - "name": "RTF to PDF", - "subtitle": "Convert Rich Text Format documents to PDF. Supports multiple files.", - "acceptedFormats": "RTF files", - "convertButton": "Convert to PDF" - }, - "wordToPdf": { - "name": "Word to PDF", - "subtitle": "Convert Word documents (DOCX, DOC, ODT, RTF) to PDF format. Supports multiple files.", - "acceptedFormats": "DOCX, DOC, ODT, RTF files", - "convertButton": "Convert to PDF" - }, - "excelToPdf": { - "name": "Excel to PDF", - "subtitle": "Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. Supports multiple files.", - "acceptedFormats": "XLSX, XLS, ODS, CSV files", - "convertButton": "Convert to PDF" - }, - "powerpointToPdf": { - "name": "PowerPoint to PDF", - "subtitle": "Convert PowerPoint presentations (PPTX, PPT, ODP) to PDF format. Supports multiple files.", - "acceptedFormats": "PPTX, PPT, ODP files", - "convertButton": "Convert to PDF" - }, - "markdownToPdf": { - "name": "Markdown to PDF", - "subtitle": "Write or paste Markdown and export it as a beautifully formatted PDF.", - "paneMarkdown": "Markdown", - "panePreview": "Preview", - "btnUpload": "Upload", - "btnSyncScroll": "Sync Scroll", - "btnSettings": "Settings", - "btnExportPdf": "Export PDF", - "settingsTitle": "Markdown Settings", - "settingsPreset": "Preset", - "presetDefault": "Default (GFM-like)", - "presetCommonmark": "CommonMark (strict)", - "presetZero": "Minimal (no features)", - "settingsOptions": "Markdown Options", - "optAllowHtml": "Allow HTML tags", - "optBreaks": "Convert newlines to
", - "optLinkify": "Auto-convert URLs to links", - "optTypographer": "Typographer (smart quotes, etc.)" - }, - "pdfBooklet": { - "name": "PDF Booklet", - "subtitle": "Rearrange pages for double-sided booklet printing. Fold and staple to create a booklet.", - "howItWorks": "How it works:", - "step1": "Upload a PDF file.", - "step2": "Pages will be rearranged in booklet order.", - "step3": "Print double-sided, flip on short edge, fold and staple.", - "paperSize": "Paper Size", - "orientation": "Orientation", - "portrait": "Portrait", - "landscape": "Landscape", - "pagesPerSheet": "Pages per Sheet", - "createBooklet": "Create Booklet", - "processing": "Processing...", - "pageCount": "Page count will be padded to multiple of 4 if needed." - }, - "xpsToPdf": { - "name": "XPS to PDF", - "subtitle": "Convert XPS/OXPS documents to PDF format. Supports multiple files.", - "acceptedFormats": "XPS, OXPS files", - "convertButton": "Convert to PDF" - }, - "mobiToPdf": { - "name": "MOBI to PDF", - "subtitle": "Convert MOBI e-books to PDF format. Supports multiple files.", - "acceptedFormats": "MOBI files", - "convertButton": "Convert to PDF" - }, - "epubToPdf": { - "name": "EPUB to PDF", - "subtitle": "Convert EPUB e-books to PDF format. Supports multiple files.", - "acceptedFormats": "EPUB files", - "convertButton": "Convert to PDF" - }, - "fb2ToPdf": { - "name": "FB2 to PDF", - "subtitle": "Convert FictionBook (FB2) e-books to PDF format. Supports multiple files.", - "acceptedFormats": "FB2 files", - "convertButton": "Convert to PDF" - }, - "cbzToPdf": { - "name": "CBZ to PDF", - "subtitle": "Convert comic book archives (CBZ/CBR) to PDF format. Supports multiple files.", - "acceptedFormats": "CBZ, CBR files", - "convertButton": "Convert to PDF" - }, - "wpdToPdf": { - "name": "WPD to PDF", - "subtitle": "Convert WordPerfect documents (WPD) to PDF format. Supports multiple files.", - "acceptedFormats": "WPD files", - "convertButton": "Convert to PDF" - }, - "wpsToPdf": { - "name": "WPS to PDF", - "subtitle": "Convert WPS Office documents to PDF format. Supports multiple files.", - "acceptedFormats": "WPS files", - "convertButton": "Convert to PDF" - }, - "xmlToPdf": { - "name": "XML to PDF", - "subtitle": "Convert XML documents to PDF format. Supports multiple files.", - "acceptedFormats": "XML files", - "convertButton": "Convert to PDF" - }, - "pagesToPdf": { - "name": "Pages to PDF", - "subtitle": "Convert Apple Pages documents to PDF format. Supports multiple files.", - "acceptedFormats": "Pages files", - "convertButton": "Convert to PDF" - }, - "odgToPdf": { - "name": "ODG to PDF", - "subtitle": "Convert OpenDocument Graphics (ODG) files to PDF format. Supports multiple files.", - "acceptedFormats": "ODG files", - "convertButton": "Convert to PDF" - }, - "odsToPdf": { - "name": "ODS to PDF", - "subtitle": "Convert OpenDocument Spreadsheet (ODS) files to PDF format. Supports multiple files.", - "acceptedFormats": "ODS files", - "convertButton": "Convert to PDF" - }, - "odpToPdf": { - "name": "ODP to PDF", - "subtitle": "Convert OpenDocument Presentation (ODP) files to PDF format. Supports multiple files.", - "acceptedFormats": "ODP files", - "convertButton": "Convert to PDF" - }, - "pubToPdf": { - "name": "PUB to PDF", - "subtitle": "Convert Microsoft Publisher (PUB) files to PDF format. Supports multiple files.", - "acceptedFormats": "PUB files", - "convertButton": "Convert to PDF" - }, - "vsdToPdf": { - "name": "VSD to PDF", - "subtitle": "Convert Microsoft Visio (VSD, VSDX) files to PDF format. Supports multiple files.", - "acceptedFormats": "VSD, VSDX files", - "convertButton": "Convert to PDF" - }, - "psdToPdf": { - "name": "PSD to PDF", - "subtitle": "Convert Adobe Photoshop (PSD) files to PDF format. Supports multiple files.", - "acceptedFormats": "PSD files", - "convertButton": "Convert to PDF" - }, - "pdfToSvg": { - "name": "PDF to SVG", - "subtitle": "Convert each page of a PDF file into a scalable vector graphic (SVG) for perfect quality at any size." - }, - "extractTables": { - "name": "Extract PDF Tables", - "subtitle": "Extract tables from PDF files and export as CSV, JSON, or Markdown." - }, - "pdfToCsv": { - "name": "PDF to CSV", - "subtitle": "Extract tables from PDF and convert to CSV format." - }, - "pdfToExcel": { - "name": "PDF to Excel", - "subtitle": "Extract tables from PDF and convert to Excel (XLSX) format." - }, - "pdfToText": { - "name": "PDF to Text", - "subtitle": "Extract text from PDF files and save as plain text (.txt). Supports multiple files.", - "note": "This tool works ONLY with digitally created PDFs. For scanned documents or image-based PDFs, use our OCR PDF tool instead.", - "convertButton": "Extract Text" - }, - "digitalSignPdf": { - "name": "Digital Signature PDF", - "pageTitle": "Digital Signature PDF - Add Cryptographic Signature | BentoPDF", - "subtitle": "Add a cryptographic digital signature to your PDF using X.509 certificates. Supports PKCS#12 (.pfx, .p12) and PEM formats. Your private key never leaves your browser.", - "certificateSection": "Certificate", - "uploadCert": "Upload certificate (.pfx, .p12)", - "certPassword": "Certificate Password", - "certPasswordPlaceholder": "Enter certificate password", - "certInfo": "Certificate Information", - "certSubject": "Subject", - "certIssuer": "Issuer", - "certValidity": "Valid", - "signatureDetails": "Signature Details (Optional)", - "reason": "Reason", - "reasonPlaceholder": "e.g., I approve this document", - "location": "Location", - "locationPlaceholder": "e.g., New York, USA", - "contactInfo": "Contact Info", - "contactPlaceholder": "e.g., email@example.com", - "applySignature": "Apply Digital Signature", - "successMessage": "PDF signed successfully! The signature can be verified in any PDF reader." - }, - "validateSignaturePdf": { - "name": "Validate PDF Signature", - "pageTitle": "Validate PDF Signature - Verify Digital Signatures | BentoPDF", - "subtitle": "Verify digital signatures in your PDF files. Check certificate validity, view signer details, and confirm document integrity. All processing happens in your browser." - } -} \ No newline at end of file + "categories": { + "popularTools": "Popular Tools", + "editAnnotate": "Edit & Annotate", + "convertToPdf": "Convert to PDF", + "convertFromPdf": "Convert from PDF", + "organizeManage": "Organize & Manage", + "optimizeRepair": "Optimize & Repair", + "securePdf": "Secure PDF" + }, + "pdfMultiTool": { + "name": "PDF Multi Tool", + "subtitle": "Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface." + }, + "mergePdf": { + "name": "Merge PDF", + "subtitle": "Combine multiple PDFs into one file. Preserves Bookmarks." + }, + "splitPdf": { + "name": "Split PDF", + "subtitle": "Extract a range of pages into a new PDF." + }, + "compressPdf": { + "name": "Compress PDF", + "subtitle": "Reduce the file size of your PDF.", + "algorithmLabel": "Compression Algorithm", + "condense": "Condense (Recommended)", + "photon": "Photon (For Photo-Heavy PDFs)", + "condenseInfo": "Condense uses advanced compression: removes dead-weight, optimizes images, subsets fonts. Best for most PDFs.", + "photonInfo": "Photon converts pages to images. Use for photo-heavy/scanned PDFs.", + "photonWarning": "Warning: Text will become non-selectable and links will stop working.", + "levelLabel": "Compression Level", + "light": "Light (Preserve Quality)", + "balanced": "Balanced (Recommended)", + "aggressive": "Aggressive (Smaller Files)", + "extreme": "Extreme (Maximum Compression)", + "grayscale": "Convert to Grayscale", + "grayscaleHint": "Reduces file size by removing color information", + "customSettings": "Custom Settings", + "customSettingsHint": "Fine-tune compression parameters:", + "outputQuality": "Output Quality", + "resizeImagesTo": "Resize Images To", + "onlyProcessAbove": "Only Process Above", + "removeMetadata": "Remove metadata", + "subsetFonts": "Subset fonts (remove unused glyphs)", + "removeThumbnails": "Remove embedded thumbnails", + "compressButton": "Compress PDF" + }, + "pdfEditor": { + "name": "PDF Editor", + "subtitle": "Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs." + }, + "jpgToPdf": { + "name": "JPG to PDF", + "subtitle": "Create a PDF from JPG, JPEG, and JPEG2000 (JP2/JPX) images." + }, + "signPdf": { + "name": "Sign PDF", + "subtitle": "Draw, type, or upload your signature." + }, + "cropPdf": { + "name": "Crop PDF", + "subtitle": "Trim the margins of every page in your PDF." + }, + "extractPages": { + "name": "Extract Pages", + "subtitle": "Save a selection of pages as new files." + }, + "duplicateOrganize": { + "name": "Duplicate & Organize", + "subtitle": "Duplicate, reorder, and delete pages." + }, + "deletePages": { + "name": "Delete Pages", + "subtitle": "Remove specific pages from your document." + }, + "editBookmarks": { + "name": "Edit Bookmarks", + "subtitle": "Add, edit, import, delete and extract PDF bookmarks." + }, + "tableOfContents": { + "name": "Table of Contents", + "subtitle": "Generate a table of contents page from PDF bookmarks." + }, + "pageNumbers": { + "name": "Page Numbers", + "subtitle": "Insert page numbers into your document." + }, + "addWatermark": { + "name": "Add Watermark", + "subtitle": "Stamp text or an image over your PDF pages." + }, + "headerFooter": { + "name": "Header & Footer", + "subtitle": "Add text to the top and bottom of pages." + }, + "invertColors": { + "name": "Invert Colors", + "subtitle": "Create a \"dark mode\" version of your PDF." + }, + "backgroundColor": { + "name": "Background Color", + "subtitle": "Change the background color of your PDF." + }, + "changeTextColor": { + "name": "Change Text Color", + "subtitle": "Change the color of text in your PDF." + }, + "addStamps": { + "name": "Add Stamps", + "subtitle": "Add image stamps to your PDF using the annotation toolbar.", + "usernameLabel": "Stamp Username", + "usernamePlaceholder": "Enter your name (for stamps)", + "usernameHint": "This name will appear on stamps you create." + }, + "removeAnnotations": { + "name": "Remove Annotations", + "subtitle": "Strip comments, highlights, and links." + }, + "pdfFormFiller": { + "name": "PDF Form Filler", + "subtitle": "Fill in forms directly in the browser. Also supports XFA forms." + }, + "createPdfForm": { + "name": "Create PDF Form", + "subtitle": "Create fillable PDF forms with drag-and-drop text fields." + }, + "removeBlankPages": { + "name": "Remove Blank Pages", + "subtitle": "Automatically detect and delete blank pages." + }, + "imageToPdf": { + "name": "Images to PDF", + "subtitle": "Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP to PDF." + }, + "pngToPdf": { + "name": "PNG to PDF", + "subtitle": "Create a PDF from one or more PNG images." + }, + "webpToPdf": { + "name": "WebP to PDF", + "subtitle": "Create a PDF from one or more WebP images." + }, + "svgToPdf": { + "name": "SVG to PDF", + "subtitle": "Create a PDF from one or more SVG images." + }, + "bmpToPdf": { + "name": "BMP to PDF", + "subtitle": "Create a PDF from one or more BMP images." + }, + "heicToPdf": { + "name": "HEIC to PDF", + "subtitle": "Create a PDF from one or more HEIC images." + }, + "tiffToPdf": { + "name": "TIFF to PDF", + "subtitle": "Create a PDF from one or more TIFF images." + }, + "textToPdf": { + "name": "Text to PDF", + "subtitle": "Convert a plain text file into a PDF." + }, + "jsonToPdf": { + "name": "JSON to PDF", + "subtitle": "Convert JSON files to PDF format." + }, + "pdfToJpg": { + "name": "PDF to JPG", + "subtitle": "Convert each PDF page into a JPG image." + }, + "pdfToPng": { + "name": "PDF to PNG", + "subtitle": "Convert each PDF page into a PNG image." + }, + "pdfToWebp": { + "name": "PDF to WebP", + "subtitle": "Convert each PDF page into a WebP image." + }, + "pdfToBmp": { + "name": "PDF to BMP", + "subtitle": "Convert each PDF page into a BMP image." + }, + "pdfToTiff": { + "name": "PDF to TIFF", + "subtitle": "Convert each PDF page into a TIFF image." + }, + "pdfToGreyscale": { + "name": "PDF to Greyscale", + "subtitle": "Convert all colors to black and white." + }, + "pdfToJson": { + "name": "PDF to JSON", + "subtitle": "Convert PDF files to JSON format." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Make a PDF searchable and copyable." + }, + "alternateMix": { + "name": "Alternate & Mix Pages", + "subtitle": "Merge PDFs by alternating pages from each PDF. Preserves Bookmarks." + }, + "addAttachments": { + "name": "Add Attachments", + "subtitle": "Embed one or more files into your PDF." + }, + "extractAttachments": { + "name": "Extract Attachments", + "subtitle": "Extract all embedded files from PDF(s) as a ZIP." + }, + "editAttachments": { + "name": "Edit Attachments", + "subtitle": "View or remove attachments in your PDF." + }, + "dividePages": { + "name": "Divide Pages", + "subtitle": "Divide pages horizontally or vertically." + }, + "addBlankPage": { + "name": "Add Blank Page", + "subtitle": "Insert an empty page anywhere in your PDF." + }, + "reversePages": { + "name": "Reverse Pages", + "subtitle": "Flip the order of all pages in your document." + }, + "rotatePdf": { + "name": "Rotate PDF", + "subtitle": "Turn pages in 90-degree increments." + }, + "rotateCustom": { + "name": "Rotate by Custom Degrees", + "subtitle": "Rotate pages by any custom angle." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Arrange multiple pages onto a single sheet." + }, + "combineToSinglePage": { + "name": "Combine to Single Page", + "subtitle": "Stitch all pages into one continuous scroll." + }, + "viewMetadata": { + "name": "View Metadata", + "subtitle": "Inspect the hidden properties of your PDF." + }, + "editMetadata": { + "name": "Edit Metadata", + "subtitle": "Change the author, title, and other properties." + }, + "pdfsToZip": { + "name": "PDFs to ZIP", + "subtitle": "Package multiple PDF files into a ZIP archive." + }, + "comparePdfs": { + "name": "Compare PDFs", + "subtitle": "Compare two PDFs side by side." + }, + "posterizePdf": { + "name": "Posterize PDF", + "subtitle": "Split a large page into multiple smaller pages." + }, + "fixPageSize": { + "name": "Fix Page Size", + "subtitle": "Standardize all pages to a uniform size." + }, + "linearizePdf": { + "name": "Linearize PDF", + "subtitle": "Optimize PDF for fast web viewing." + }, + "pageDimensions": { + "name": "Page Dimensions", + "subtitle": "Analyze page size, orientation, and units." + }, + "removeRestrictions": { + "name": "Remove Restrictions", + "subtitle": "Remove password protection and security restrictions associated with digitally signed PDF files." + }, + "repairPdf": { + "name": "Repair PDF", + "subtitle": "Recover data from corrupted or damaged PDF files." + }, + "encryptPdf": { + "name": "Encrypt PDF", + "subtitle": "Lock your PDF by adding a password." + }, + "sanitizePdf": { + "name": "Sanitize PDF", + "subtitle": "Remove metadata, annotations, scripts, and more." + }, + "decryptPdf": { + "name": "Decrypt PDF", + "subtitle": "Unlock PDF by removing password protection." + }, + "flattenPdf": { + "name": "Flatten PDF", + "subtitle": "Make form fields and annotations non-editable." + }, + "removeMetadata": { + "name": "Remove Metadata", + "subtitle": "Strip hidden data from your PDF." + }, + "changePermissions": { + "name": "Change Permissions", + "subtitle": "Set or change user permissions on a PDF." + }, + "odtToPdf": { + "name": "ODT to PDF", + "subtitle": "Convert OpenDocument Text files to PDF format. Supports multiple files.", + "acceptedFormats": "ODT files", + "convertButton": "Convert to PDF" + }, + "csvToPdf": { + "name": "CSV to PDF", + "subtitle": "Convert CSV spreadsheet files to PDF format. Supports multiple files.", + "acceptedFormats": "CSV files", + "convertButton": "Convert to PDF" + }, + "rtfToPdf": { + "name": "RTF to PDF", + "subtitle": "Convert Rich Text Format documents to PDF. Supports multiple files.", + "acceptedFormats": "RTF files", + "convertButton": "Convert to PDF" + }, + "wordToPdf": { + "name": "Word to PDF", + "subtitle": "Convert Word documents (DOCX, DOC, ODT, RTF) to PDF format. Supports multiple files.", + "acceptedFormats": "DOCX, DOC, ODT, RTF files", + "convertButton": "Convert to PDF" + }, + "excelToPdf": { + "name": "Excel to PDF", + "subtitle": "Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. Supports multiple files.", + "acceptedFormats": "XLSX, XLS, ODS, CSV files", + "convertButton": "Convert to PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint to PDF", + "subtitle": "Convert PowerPoint presentations (PPTX, PPT, ODP) to PDF format. Supports multiple files.", + "acceptedFormats": "PPTX, PPT, ODP files", + "convertButton": "Convert to PDF" + }, + "markdownToPdf": { + "name": "Markdown to PDF", + "subtitle": "Write or paste Markdown and export it as a beautifully formatted PDF.", + "paneMarkdown": "Markdown", + "panePreview": "Preview", + "btnUpload": "Upload", + "btnSyncScroll": "Sync Scroll", + "btnSettings": "Settings", + "btnExportPdf": "Export PDF", + "settingsTitle": "Markdown Settings", + "settingsPreset": "Preset", + "presetDefault": "Default (GFM-like)", + "presetCommonmark": "CommonMark (strict)", + "presetZero": "Minimal (no features)", + "settingsOptions": "Markdown Options", + "optAllowHtml": "Allow HTML tags", + "optBreaks": "Convert newlines to
", + "optLinkify": "Auto-convert URLs to links", + "optTypographer": "Typographer (smart quotes, etc.)" + }, + "pdfBooklet": { + "name": "PDF Booklet", + "subtitle": "Rearrange pages for double-sided booklet printing. Fold and staple to create a booklet.", + "howItWorks": "How it works:", + "step1": "Upload a PDF file.", + "step2": "Pages will be rearranged in booklet order.", + "step3": "Print double-sided, flip on short edge, fold and staple.", + "paperSize": "Paper Size", + "orientation": "Orientation", + "portrait": "Portrait", + "landscape": "Landscape", + "pagesPerSheet": "Pages per Sheet", + "createBooklet": "Create Booklet", + "processing": "Processing...", + "pageCount": "Page count will be padded to multiple of 4 if needed." + }, + "xpsToPdf": { + "name": "XPS to PDF", + "subtitle": "Convert XPS/OXPS documents to PDF format. Supports multiple files.", + "acceptedFormats": "XPS, OXPS files", + "convertButton": "Convert to PDF" + }, + "mobiToPdf": { + "name": "MOBI to PDF", + "subtitle": "Convert MOBI e-books to PDF format. Supports multiple files.", + "acceptedFormats": "MOBI files", + "convertButton": "Convert to PDF" + }, + "epubToPdf": { + "name": "EPUB to PDF", + "subtitle": "Convert EPUB e-books to PDF format. Supports multiple files.", + "acceptedFormats": "EPUB files", + "convertButton": "Convert to PDF" + }, + "fb2ToPdf": { + "name": "FB2 to PDF", + "subtitle": "Convert FictionBook (FB2) e-books to PDF format. Supports multiple files.", + "acceptedFormats": "FB2 files", + "convertButton": "Convert to PDF" + }, + "cbzToPdf": { + "name": "CBZ to PDF", + "subtitle": "Convert comic book archives (CBZ/CBR) to PDF format. Supports multiple files.", + "acceptedFormats": "CBZ, CBR files", + "convertButton": "Convert to PDF" + }, + "wpdToPdf": { + "name": "WPD to PDF", + "subtitle": "Convert WordPerfect documents (WPD) to PDF format. Supports multiple files.", + "acceptedFormats": "WPD files", + "convertButton": "Convert to PDF" + }, + "wpsToPdf": { + "name": "WPS to PDF", + "subtitle": "Convert WPS Office documents to PDF format. Supports multiple files.", + "acceptedFormats": "WPS files", + "convertButton": "Convert to PDF" + }, + "xmlToPdf": { + "name": "XML to PDF", + "subtitle": "Convert XML documents to PDF format. Supports multiple files.", + "acceptedFormats": "XML files", + "convertButton": "Convert to PDF" + }, + "pagesToPdf": { + "name": "Pages to PDF", + "subtitle": "Convert Apple Pages documents to PDF format. Supports multiple files.", + "acceptedFormats": "Pages files", + "convertButton": "Convert to PDF" + }, + "odgToPdf": { + "name": "ODG to PDF", + "subtitle": "Convert OpenDocument Graphics (ODG) files to PDF format. Supports multiple files.", + "acceptedFormats": "ODG files", + "convertButton": "Convert to PDF" + }, + "odsToPdf": { + "name": "ODS to PDF", + "subtitle": "Convert OpenDocument Spreadsheet (ODS) files to PDF format. Supports multiple files.", + "acceptedFormats": "ODS files", + "convertButton": "Convert to PDF" + }, + "odpToPdf": { + "name": "ODP to PDF", + "subtitle": "Convert OpenDocument Presentation (ODP) files to PDF format. Supports multiple files.", + "acceptedFormats": "ODP files", + "convertButton": "Convert to PDF" + }, + "pubToPdf": { + "name": "PUB to PDF", + "subtitle": "Convert Microsoft Publisher (PUB) files to PDF format. Supports multiple files.", + "acceptedFormats": "PUB files", + "convertButton": "Convert to PDF" + }, + "vsdToPdf": { + "name": "VSD to PDF", + "subtitle": "Convert Microsoft Visio (VSD, VSDX) files to PDF format. Supports multiple files.", + "acceptedFormats": "VSD, VSDX files", + "convertButton": "Convert to PDF" + }, + "psdToPdf": { + "name": "PSD to PDF", + "subtitle": "Convert Adobe Photoshop (PSD) files to PDF format. Supports multiple files.", + "acceptedFormats": "PSD files", + "convertButton": "Convert to PDF" + }, + "pdfToSvg": { + "name": "PDF to SVG", + "subtitle": "Convert each page of a PDF file into a scalable vector graphic (SVG) for perfect quality at any size." + }, + "extractTables": { + "name": "Extract PDF Tables", + "subtitle": "Extract tables from PDF files and export as CSV, JSON, or Markdown." + }, + "pdfToCsv": { + "name": "PDF to CSV", + "subtitle": "Extract tables from PDF and convert to CSV format." + }, + "pdfToExcel": { + "name": "PDF to Excel", + "subtitle": "Extract tables from PDF and convert to Excel (XLSX) format." + }, + "pdfToText": { + "name": "PDF to Text", + "subtitle": "Extract text from PDF files and save as plain text (.txt). Supports multiple files.", + "note": "This tool works ONLY with digitally created PDFs. For scanned documents or image-based PDFs, use our OCR PDF tool instead.", + "convertButton": "Extract Text" + }, + "digitalSignPdf": { + "name": "Digital Signature PDF", + "pageTitle": "Digital Signature PDF - Add Cryptographic Signature | BentoPDF", + "subtitle": "Add a cryptographic digital signature to your PDF using X.509 certificates. Supports PKCS#12 (.pfx, .p12) and PEM formats. Your private key never leaves your browser.", + "certificateSection": "Certificate", + "uploadCert": "Upload certificate (.pfx, .p12)", + "certPassword": "Certificate Password", + "certPasswordPlaceholder": "Enter certificate password", + "certInfo": "Certificate Information", + "certSubject": "Subject", + "certIssuer": "Issuer", + "certValidity": "Valid", + "signatureDetails": "Signature Details (Optional)", + "reason": "Reason", + "reasonPlaceholder": "e.g., I approve this document", + "location": "Location", + "locationPlaceholder": "e.g., New York, USA", + "contactInfo": "Contact Info", + "contactPlaceholder": "e.g., email@example.com", + "applySignature": "Apply Digital Signature", + "successMessage": "PDF signed successfully! The signature can be verified in any PDF reader." + }, + "validateSignaturePdf": { + "name": "Validate PDF Signature", + "pageTitle": "Validate PDF Signature - Verify Digital Signatures | BentoPDF", + "subtitle": "Verify digital signatures in your PDF files. Check certificate validity, view signer details, and confirm document integrity. All processing happens in your browser." + }, + "emailToPdf": { + "name": "Email to PDF", + "subtitle": "Convert email files (EML, MSG) to PDF format. Supports Outlook exports and standard email formats.", + "acceptedFormats": "EML, MSG files", + "convertButton": "Convert to PDF" + } +} diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index f1e27ec23..497ccdcf3 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -515,5 +515,11 @@ "name": "Validasi Tanda Tangan PDF", "pageTitle": "Validasi Tanda Tangan PDF - Verifikasi Tanda Tangan Digital | BentoPDF", "subtitle": "Verifikasi tanda tangan digital di file PDF Anda. Periksa validitas sertifikat, lihat detail penandatangan, dan konfirmasi integritas dokumen. Semua pemrosesan terjadi di browser Anda." + }, + "emailToPdf": { + "name": "Email ke PDF", + "subtitle": "Konversi file email (EML, MSG) ke format PDF. Mendukung ekspor Outlook dan format email standar.", + "acceptedFormats": "File EML, MSG", + "convertButton": "Konversi ke PDF" } } diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 4835327f0..1a9960b91 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -1,492 +1,498 @@ { - "categories": { - "popularTools": "Strumenti popolari", - "editAnnotate": "Modifica e Annota", - "convertToPdf": "Converti in PDF", - "convertFromPdf": "Convert da PDF", - "organizeManage": "Organizza e Gestisci", - "optimizeRepair": "Ottimizza e Ripara", - "securePdf": "Proteggi PDF" - }, - "pdfMultiTool": { - "name": "PDF Multi Tool", - "subtitle": "Unisci, Dividi, Organizza, Elimina, Ruota, Aggiungi Pagine Vuote, Estrai e Duplica in un'interfaccia unificata." - }, - "mergePdf": { - "name": "Unisci PDF", - "subtitle": "Unisci più PDF in un unico file. Conserva i Segnalibri." - }, - "splitPdf": { - "name": "Dividi PDF", - "subtitle": "Estrai un insieme di pagine in un nuovo PDF." - }, - "compressPdf": { - "name": "Comprimi PDF", - "subtitle": "Riduci le dimensioni del tuo PDF.", - "algorithmLabel": "Algoritmo di compressione", - "condense": "Condensa (Consigliato)", - "photon": "Photon (Per PDF con molte foto)", - "condenseInfo": "Condensa usa la compressione avanzata: rimuove peso inutile, ottimizza le immagini, riduce i font. Migliore per la maggior parte dei PDF.", - "photonInfo": "Photon converte le pagine in immagini. Usalo per PDF con molte foto o scannerizzati.", - "photonWarning": "Attenzione: il testo non sarà selezionabile e i link smetteranno di funzionare.", - "levelLabel": "Livello di compressione", - "light": "Leggero (Preserva qualità)", - "balanced": "Bilanciato (Consigliato)", - "aggressive": "Aggressivo (File più piccoli)", - "extreme": "Estremo (Compressione massima)", - "grayscale": "Converti in scala di grigi", - "grayscaleHint": "Riduce le dimensioni rimuovendo le informazioni di colore", - "customSettings": "Impostazioni personalizzate", - "customSettingsHint": "Affina i parametri di compressione:", - "outputQuality": "Qualità di output", - "resizeImagesTo": "Ridimensiona le immagini a", - "onlyProcessAbove": "Elabora solo sopra", - "removeMetadata": "Rimuovi metadati", - "subsetFonts": "Riduci i font (rimuovi glifi non usati)", - "removeThumbnails": "Rimuovi miniature incorporate", - "compressButton": "Comprimi PDF" - }, - "pdfEditor": { - "name": "Editor PDF", - "subtitle": "Annota, evidenzia, redigi, commenta, aggiungi forme/immagini, cerca e visualizza PDF." - }, - "jpgToPdf": { - "name": "JPG in PDF", - "subtitle": "Crea un PDF da immagini JPG, JPEG e JPEG2000 (JP2/JPX)." - }, - "signPdf": { - "name": "Firma PDF", - "subtitle": "Disegna, digita o carica la tua firma." - }, - "cropPdf": { - "name": "Ritaglia PDF", - "subtitle": "Rimuovi i margini di ogni pagina del tuo PDF." - }, - "extractPages": { - "name": "Estrai Pagine", - "subtitle": "Salva una selezione di pagine come nuovi file." - }, - "duplicateOrganize": { - "name": "Duplica e Organizza", - "subtitle": "Duplica, riordina e elimina pagine." - }, - "deletePages": { - "name": "Elimina Pagine", - "subtitle": "Rimuovi pagine specifiche dal tuo documento." - }, - "editBookmarks": { - "name": "Modifica Segnalibri", - "subtitle": "Aggiungi, modifica, importa, elimina ed estrai segnalibri PDF." - }, - "tableOfContents": { - "name": "Indice", - "subtitle": "Genera una pagina di indice dai segnalibri del PDF." - }, - "pageNumbers": { - "name": "Numeri di Pagina", - "subtitle": "Inserisci i numeri di pagina nel tuo documento." - }, - "addWatermark": { - "name": "Aggiungi Filigrana", - "subtitle": "Applica testo o un'immagine sulle pagine del tuo PDF." - }, - "headerFooter": { - "name": "Intestazione e Piè di Pagina", - "subtitle": "Aggiungi testo nella parte superiore e inferiore delle pagine." - }, - "invertColors": { - "name": "Inverti Colori", - "subtitle": "Crea una versione \"modalità scura\" del tuo PDF." - }, - "backgroundColor": { - "name": "Colore di Sfondo", - "subtitle": "Cambia il colore di sfondo del tuo PDF." - }, - "changeTextColor": { - "name": "Cambia Colore Testo", - "subtitle": "Cambia il colore del testo nel tuo PDF." - }, - "addStamps": { - "name": "Aggiungi Timbri", - "subtitle": "Aggiungi timbri immagine al tuo PDF usando la barra degli strumenti di annotazione.", - "usernameLabel": "Nome sul timbro", - "usernamePlaceholder": "Inserisci il tuo nome (per i timbri)", - "usernameHint": "Questo nome apparirà sui timbri che crei." - }, - "removeAnnotations": { - "name": "Rimuovi Annotazioni", - "subtitle": "Rimuovi commenti, evidenziazioni e link." - }, - "pdfFormFiller": { - "name": "Compilatore Moduli PDF", - "subtitle": "Compila i moduli direttamente nel browser. Supporta anche i moduli XFA." - }, - "createPdfForm": { - "name": "Crea Modulo PDF", - "subtitle": "Crea moduli PDF compilabili con campi di testo drag-and-drop." - }, - "removeBlankPages": { - "name": "Rimuovi Pagine Vuote", - "subtitle": "Rileva e elimina automaticamente le pagine vuote." - }, - "imageToPdf": { - "name": "Immagini in PDF", - "subtitle": "Converti JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP in PDF." - }, - "pngToPdf": { - "name": "PNG in PDF", - "subtitle": "Crea un PDF da una o più immagini PNG." - }, - "webpToPdf": { - "name": "WebP in PDF", - "subtitle": "Crea un PDF da una o più immagini WebP." - }, - "svgToPdf": { - "name": "SVG in PDF", - "subtitle": "Crea un PDF da una o più immagini SVG." - }, - "bmpToPdf": { - "name": "BMP in PDF", - "subtitle": "Crea un PDF da una o più immagini BMP." - }, - "heicToPdf": { - "name": "HEIC in PDF", - "subtitle": "Crea un PDF da una o più immagini HEIC." - }, - "tiffToPdf": { - "name": "TIFF in PDF", - "subtitle": "Crea un PDF da una o più immagini TIFF." - }, - "textToPdf": { - "name": "Testo in PDF", - "subtitle": "Converti un file di testo semplice in un PDF." - }, - "jsonToPdf": { - "name": "JSON in PDF", - "subtitle": "Converti file JSON in formato PDF." - }, - "pdfToJpg": { - "name": "PDF in JPG", - "subtitle": "Converti ogni pagina del PDF in un'immagine JPG." - }, - "pdfToPng": { - "name": "PDF in PNG", - "subtitle": "Converti ogni pagina del PDF in un'immagine PNG." - }, - "pdfToWebp": { - "name": "PDF in WebP", - "subtitle": "Converti ogni pagina del PDF in un'immagine WebP." - }, - "pdfToBmp": { - "name": "PDF in BMP", - "subtitle": "Converti ogni pagina del PDF in un'immagine BMP." - }, - "pdfToTiff": { - "name": "PDF in TIFF", - "subtitle": "Converti ogni pagina del PDF in un'immagine TIFF." - }, - "pdfToGreyscale": { - "name": "PDF in Scala di Grigi", - "subtitle": "Converti tutti i colori in scala di grigi." - }, - "pdfToJson": { - "name": "PDF in JSON", - "subtitle": "Converti file PDF in formato JSON." - }, - "ocrPdf": { - "name": "OCR PDF", - "subtitle": "Rendi un PDF ricercabile e copiabile." - }, - "alternateMix": { - "name": "Alterna e Riordina Pagine", - "subtitle": "Unisci PDF sostituendo le pagine di ogni file. Conserva i segnalibri." - }, - "addAttachments": { - "name": "Aggiungi Allegati", - "subtitle": "Incorpora uno o più file nel tuo PDF." - }, - "extractAttachments": { - "name": "Estrai Allegati", - "subtitle": "Estrai tutti i file incorporati dai PDF come archivio ZIP." - }, - "editAttachments": { - "name": "Modifica Allegati", - "subtitle": "Visualizza o rimuovi gli allegati nel tuo PDF." - }, - "dividePages": { - "name": "Dividi Pagine", - "subtitle": "Dividi le pagine orizzontalmente o verticalmente." - }, - "addBlankPage": { - "name": "Aggiungi Pagina Vuota", - "subtitle": "Inserisci una pagina vuota in qualsiasi punto del tuo PDF." - }, - "reversePages": { - "name": "Inverti Pagine", - "subtitle": "Inverti l'ordine di tutte le pagine del documento." - }, - "rotatePdf": { - "name": "Ruota PDF", - "subtitle": "Ruota le pagine per multipli di 90 gradi." - }, - "rotateCustom": { - "name": "Ruota di Gradi Personalizzati", - "subtitle": "Ruota le pagine di un angolo personalizzato." - }, - "nUpPdf": { - "name": "N-Up PDF", - "subtitle": "Disponi più pagine su un unico foglio." - }, - "combineToSinglePage": { - "name": "Combina in Una Pagina", - "subtitle": "Unisci tutte le pagine in un'unica pagina continua." - }, - "viewMetadata": { - "name": "Visualizza Metadati", - "subtitle": "Ispeziona le proprietà nascoste del tuo PDF." - }, - "editMetadata": { - "name": "Modifica Metadati", - "subtitle": "Modifica autore, titolo e altre proprietà." - }, - "pdfsToZip": { - "name": "PDF in ZIP", - "subtitle": "Raggruppa più file PDF in un archivio ZIP." - }, - "comparePdfs": { - "name": "Confronta PDF", - "subtitle": "Confronta due PDF fianco a fianco." - }, - "posterizePdf": { - "name": "Posterizza PDF", - "subtitle": "Dividi una pagina grande in più pagine più piccole." - }, - "fixPageSize": { - "name": "Correggi Dimensione Pagina", - "subtitle": "Uniforma tutte le pagine a una dimensione standard." - }, - "linearizePdf": { - "name": "Linearizza PDF", - "subtitle": "Ottimizza il PDF per una visualizzazione web più veloce." - }, - "pageDimensions": { - "name": "Dimensioni Pagina", - "subtitle": "Analizza dimensione, orientamento e unità delle pagine." - }, - "removeRestrictions": { - "name": "Rimuovi Restrizioni", - "subtitle": "Rimuovi la protezione tramite password e le restrizioni di sicurezza associate ai PDF firmati digitalmente." - }, - "repairPdf": { - "name": "Ripara PDF", - "subtitle": "Recupera i dati da file PDF corrotti o danneggiati." - }, - "encryptPdf": { - "name": "Cripta PDF", - "subtitle": "Proteggi il tuo PDF aggiungendo una password." - }, - "sanitizePdf": { - "name": "Sanitizza PDF", - "subtitle": "Rimuovi metadati, annotazioni, script e altro." - }, - "decryptPdf": { - "name": "Decrittografa PDF", - "subtitle": "Sblocca il PDF rimuovendo la protezione tramite password." - }, - "flattenPdf": { - "name": "Appiattisci PDF", - "subtitle": "Rendi i campi dei moduli e le annotazioni non modificabili." - }, - "removeMetadata": { - "name": "Rimuovi metadati", - "subtitle": "Rimuovi i dati nascosti dal tuo PDF." - }, - "changePermissions": { - "name": "Modifica permessi", - "subtitle": "Imposta o modifica i permessi utente su un PDF." - }, - "odtToPdf": { - "name": "ODT in PDF", - "subtitle": "Converti file OpenDocument Text in formato PDF. Supporta più file.", - "acceptedFormats": "File ODT", - "convertButton": "Converti in PDF" - }, - "csvToPdf": { - "name": "CSV in PDF", - "subtitle": "Converti file di foglio di calcolo CSV in formato PDF. Supporta più file.", - "acceptedFormats": "File CSV", - "convertButton": "Converti in PDF" - }, - "rtfToPdf": { - "name": "RTF in PDF", - "subtitle": "Converti documenti Rich Text Format in PDF. Supporta più file.", - "acceptedFormats": "File RTF", - "convertButton": "Converti in PDF" - }, - "wordToPdf": { - "name": "Word in PDF", - "subtitle": "Converti documenti Word (DOCX, DOC, ODT, RTF) in formato PDF. Supporta più file.", - "acceptedFormats": "File DOCX, DOC, ODT, RTF", - "convertButton": "Converti in PDF" - }, - "excelToPdf": { - "name": "Excel in PDF", - "subtitle": "Converti fogli Excel (XLSX, XLS, ODS, CSV) in formato PDF. Supporta più file.", - "acceptedFormats": "File XLSX, XLS, ODS, CSV", - "convertButton": "Converti in PDF" - }, - "powerpointToPdf": { - "name": "PowerPoint in PDF", - "subtitle": "Converti presentazioni PowerPoint (PPTX, PPT, ODP) in formato PDF. Supporta più file.", - "acceptedFormats": "File PPTX, PPT, ODP", - "convertButton": "Converti in PDF" - }, - "markdownToPdf": { - "name": "Markdown in PDF", - "subtitle": "Scrivi o incolla Markdown ed esportalo come un PDF ben formattato.", - "paneMarkdown": "Markdown", - "panePreview": "Anteprima", - "btnUpload": "Carica", - "btnSyncScroll": "Sincronizza scorrimento", - "btnSettings": "Impostazioni", - "btnExportPdf": "Esporta PDF", - "settingsTitle": "Impostazioni Markdown", - "settingsPreset": "Preset", - "presetDefault": "Predefinito (simile a GFM)", - "presetCommonmark": "CommonMark (rigoroso)", - "presetZero": "Minimale (nessuna funzionalità)", - "settingsOptions": "Opzioni Markdown", - "optAllowHtml": "Consenti tag HTML", - "optBreaks": "Converti nuove righe in
", - "optLinkify": "Converti automaticamente gli URL in link", - "optTypographer": "Tipografia (virgolette intelligenti, ecc.)" - }, - "pdfBooklet": { - "name": "Opuscolo PDF", - "subtitle": "Riorganizza le pagine per la stampa di opuscoli fronte-retro. Piega e pinza per creare un opuscolo.", - "howItWorks": "Come funziona:", - "step1": "Carica un file PDF.", - "step2": "Le pagine saranno riorganizzate in ordine per opuscolo.", - "step3": "Stampa fronte-retro, capovolgi sul lato corto, piega e pinza.", - "paperSize": "Formato carta", - "orientation": "Orientamento", - "portrait": "Ritratto", - "landscape": "Paesaggio", - "pagesPerSheet": "Pagine per foglio", - "createBooklet": "Crea opuscolo", - "processing": "Elaborazione...", - "pageCount": "Il conteggio delle pagine verrà arrotondato ad un multiplo di 4 se necessario." - }, - "xpsToPdf": { - "name": "XPS in PDF", - "subtitle": "Converti documenti XPS/OXPS in formato PDF. Supporta più file.", - "acceptedFormats": "File XPS, OXPS", - "convertButton": "Converti in PDF" - }, - "mobiToPdf": { - "name": "MOBI in PDF", - "subtitle": "Converti e-book MOBI in formato PDF. Supporta più file.", - "acceptedFormats": "File MOBI", - "convertButton": "Converti in PDF" - }, - "epubToPdf": { - "name": "EPUB in PDF", - "subtitle": "Converti e-book EPUB in formato PDF. Supporta più file.", - "acceptedFormats": "File EPUB", - "convertButton": "Converti in PDF" - }, - "fb2ToPdf": { - "name": "FB2 in PDF", - "subtitle": "Converti e-book FictionBook (FB2) in formato PDF. Supporta più file.", - "acceptedFormats": "File FB2", - "convertButton": "Converti in PDF" - }, - "cbzToPdf": { - "name": "CBZ in PDF", - "subtitle": "Converti archivi di fumetti (CBZ/CBR) in formato PDF. Supporta più file.", - "acceptedFormats": "File CBZ, CBR", - "convertButton": "Converti in PDF" - }, - "wpdToPdf": { - "name": "WPD in PDF", - "subtitle": "Converti documenti WordPerfect (WPD) in formato PDF. Supporta più file.", - "acceptedFormats": "File WPD", - "convertButton": "Converti in PDF" - }, - "wpsToPdf": { - "name": "WPS in PDF", - "subtitle": "Converti documenti WPS Office in formato PDF. Supporta più file.", - "acceptedFormats": "File WPS", - "convertButton": "Converti in PDF" - }, - "xmlToPdf": { - "name": "XML in PDF", - "subtitle": "Converti documenti XML in formato PDF. Supporta più file.", - "acceptedFormats": "File XML", - "convertButton": "Converti in PDF" - }, - "pagesToPdf": { - "name": "Pages in PDF", - "subtitle": "Converti documenti Apple Pages in formato PDF. Supporta più file.", - "acceptedFormats": "File Pages", - "convertButton": "Converti in PDF" - }, - "odgToPdf": { - "name": "ODG in PDF", - "subtitle": "Converti OpenDocument Graphics (ODG) in formato PDF. Supporta più file.", - "acceptedFormats": "File ODG", - "convertButton": "Converti in PDF" - }, - "odsToPdf": { - "name": "ODS in PDF", - "subtitle": "Converti fogli OpenDocument (ODS) in formato PDF. Supporta più file.", - "acceptedFormats": "File ODS", - "convertButton": "Converti in PDF" - }, - "odpToPdf": { - "name": "ODP in PDF", - "subtitle": "Converti presentazioni OpenDocument (ODP) in formato PDF. Supporta più file.", - "acceptedFormats": "File ODP", - "convertButton": "Converti in PDF" - }, - "pubToPdf": { - "name": "PUB in PDF", - "subtitle": "Converti file Microsoft Publisher (PUB) in formato PDF. Supporta più file.", - "acceptedFormats": "File PUB", - "convertButton": "Converti in PDF" - }, - "vsdToPdf": { - "name": "VSD in PDF", - "subtitle": "Converti file Microsoft Visio (VSD, VSDX) in formato PDF. Supporta più file.", - "acceptedFormats": "File VSD, VSDX", - "convertButton": "Converti in PDF" - }, - "psdToPdf": { - "name": "PSD in PDF", - "subtitle": "Converti file Adobe Photoshop (PSD) in formato PDF. Supporta più file.", - "acceptedFormats": "File PSD", - "convertButton": "Converti in PDF" - }, - "pdfToSvg": { - "name": "PDF in SVG", - "subtitle": "Converti ogni pagina di un file PDF in un'immagine vettoriale scalabile (SVG) per qualità perfetta a qualsiasi dimensione." - }, - "extractTables": { - "name": "Estrai tabelle PDF", - "subtitle": "Estrai le tabelle dai file PDF ed esportale come CSV, JSON o Markdown." - }, - "pdfToCsv": { - "name": "PDF in CSV", - "subtitle": "Estrai tabelle dai PDF e convertili in formato CSV." - }, - "pdfToExcel": { - "name": "PDF in Excel", - "subtitle": "Estrai tabelle dai PDF e convertili in Excel (XLSX)." - }, - "pdfToText": { - "name": "PDF in Testo", - "subtitle": "Estrai il testo dai file PDF e salvalo come testo semplice (.txt). Supporta più file.", - "note": "Questo strumento funziona SOLO con PDF creati digitalmente. Per documenti scansionati o basati su immagini, usa invece il nostro strumento OCR PDF.", - "convertButton": "Estrai testo" - } -} \ No newline at end of file + "categories": { + "popularTools": "Strumenti popolari", + "editAnnotate": "Modifica e Annota", + "convertToPdf": "Converti in PDF", + "convertFromPdf": "Convert da PDF", + "organizeManage": "Organizza e Gestisci", + "optimizeRepair": "Ottimizza e Ripara", + "securePdf": "Proteggi PDF" + }, + "pdfMultiTool": { + "name": "PDF Multi Tool", + "subtitle": "Unisci, Dividi, Organizza, Elimina, Ruota, Aggiungi Pagine Vuote, Estrai e Duplica in un'interfaccia unificata." + }, + "mergePdf": { + "name": "Unisci PDF", + "subtitle": "Unisci più PDF in un unico file. Conserva i Segnalibri." + }, + "splitPdf": { + "name": "Dividi PDF", + "subtitle": "Estrai un insieme di pagine in un nuovo PDF." + }, + "compressPdf": { + "name": "Comprimi PDF", + "subtitle": "Riduci le dimensioni del tuo PDF.", + "algorithmLabel": "Algoritmo di compressione", + "condense": "Condensa (Consigliato)", + "photon": "Photon (Per PDF con molte foto)", + "condenseInfo": "Condensa usa la compressione avanzata: rimuove peso inutile, ottimizza le immagini, riduce i font. Migliore per la maggior parte dei PDF.", + "photonInfo": "Photon converte le pagine in immagini. Usalo per PDF con molte foto o scannerizzati.", + "photonWarning": "Attenzione: il testo non sarà selezionabile e i link smetteranno di funzionare.", + "levelLabel": "Livello di compressione", + "light": "Leggero (Preserva qualità)", + "balanced": "Bilanciato (Consigliato)", + "aggressive": "Aggressivo (File più piccoli)", + "extreme": "Estremo (Compressione massima)", + "grayscale": "Converti in scala di grigi", + "grayscaleHint": "Riduce le dimensioni rimuovendo le informazioni di colore", + "customSettings": "Impostazioni personalizzate", + "customSettingsHint": "Affina i parametri di compressione:", + "outputQuality": "Qualità di output", + "resizeImagesTo": "Ridimensiona le immagini a", + "onlyProcessAbove": "Elabora solo sopra", + "removeMetadata": "Rimuovi metadati", + "subsetFonts": "Riduci i font (rimuovi glifi non usati)", + "removeThumbnails": "Rimuovi miniature incorporate", + "compressButton": "Comprimi PDF" + }, + "pdfEditor": { + "name": "Editor PDF", + "subtitle": "Annota, evidenzia, redigi, commenta, aggiungi forme/immagini, cerca e visualizza PDF." + }, + "jpgToPdf": { + "name": "JPG in PDF", + "subtitle": "Crea un PDF da immagini JPG, JPEG e JPEG2000 (JP2/JPX)." + }, + "signPdf": { + "name": "Firma PDF", + "subtitle": "Disegna, digita o carica la tua firma." + }, + "cropPdf": { + "name": "Ritaglia PDF", + "subtitle": "Rimuovi i margini di ogni pagina del tuo PDF." + }, + "extractPages": { + "name": "Estrai Pagine", + "subtitle": "Salva una selezione di pagine come nuovi file." + }, + "duplicateOrganize": { + "name": "Duplica e Organizza", + "subtitle": "Duplica, riordina e elimina pagine." + }, + "deletePages": { + "name": "Elimina Pagine", + "subtitle": "Rimuovi pagine specifiche dal tuo documento." + }, + "editBookmarks": { + "name": "Modifica Segnalibri", + "subtitle": "Aggiungi, modifica, importa, elimina ed estrai segnalibri PDF." + }, + "tableOfContents": { + "name": "Indice", + "subtitle": "Genera una pagina di indice dai segnalibri del PDF." + }, + "pageNumbers": { + "name": "Numeri di Pagina", + "subtitle": "Inserisci i numeri di pagina nel tuo documento." + }, + "addWatermark": { + "name": "Aggiungi Filigrana", + "subtitle": "Applica testo o un'immagine sulle pagine del tuo PDF." + }, + "headerFooter": { + "name": "Intestazione e Piè di Pagina", + "subtitle": "Aggiungi testo nella parte superiore e inferiore delle pagine." + }, + "invertColors": { + "name": "Inverti Colori", + "subtitle": "Crea una versione \"modalità scura\" del tuo PDF." + }, + "backgroundColor": { + "name": "Colore di Sfondo", + "subtitle": "Cambia il colore di sfondo del tuo PDF." + }, + "changeTextColor": { + "name": "Cambia Colore Testo", + "subtitle": "Cambia il colore del testo nel tuo PDF." + }, + "addStamps": { + "name": "Aggiungi Timbri", + "subtitle": "Aggiungi timbri immagine al tuo PDF usando la barra degli strumenti di annotazione.", + "usernameLabel": "Nome sul timbro", + "usernamePlaceholder": "Inserisci il tuo nome (per i timbri)", + "usernameHint": "Questo nome apparirà sui timbri che crei." + }, + "removeAnnotations": { + "name": "Rimuovi Annotazioni", + "subtitle": "Rimuovi commenti, evidenziazioni e link." + }, + "pdfFormFiller": { + "name": "Compilatore Moduli PDF", + "subtitle": "Compila i moduli direttamente nel browser. Supporta anche i moduli XFA." + }, + "createPdfForm": { + "name": "Crea Modulo PDF", + "subtitle": "Crea moduli PDF compilabili con campi di testo drag-and-drop." + }, + "removeBlankPages": { + "name": "Rimuovi Pagine Vuote", + "subtitle": "Rileva e elimina automaticamente le pagine vuote." + }, + "imageToPdf": { + "name": "Immagini in PDF", + "subtitle": "Converti JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP in PDF." + }, + "pngToPdf": { + "name": "PNG in PDF", + "subtitle": "Crea un PDF da una o più immagini PNG." + }, + "webpToPdf": { + "name": "WebP in PDF", + "subtitle": "Crea un PDF da una o più immagini WebP." + }, + "svgToPdf": { + "name": "SVG in PDF", + "subtitle": "Crea un PDF da una o più immagini SVG." + }, + "bmpToPdf": { + "name": "BMP in PDF", + "subtitle": "Crea un PDF da una o più immagini BMP." + }, + "heicToPdf": { + "name": "HEIC in PDF", + "subtitle": "Crea un PDF da una o più immagini HEIC." + }, + "tiffToPdf": { + "name": "TIFF in PDF", + "subtitle": "Crea un PDF da una o più immagini TIFF." + }, + "textToPdf": { + "name": "Testo in PDF", + "subtitle": "Converti un file di testo semplice in un PDF." + }, + "jsonToPdf": { + "name": "JSON in PDF", + "subtitle": "Converti file JSON in formato PDF." + }, + "pdfToJpg": { + "name": "PDF in JPG", + "subtitle": "Converti ogni pagina del PDF in un'immagine JPG." + }, + "pdfToPng": { + "name": "PDF in PNG", + "subtitle": "Converti ogni pagina del PDF in un'immagine PNG." + }, + "pdfToWebp": { + "name": "PDF in WebP", + "subtitle": "Converti ogni pagina del PDF in un'immagine WebP." + }, + "pdfToBmp": { + "name": "PDF in BMP", + "subtitle": "Converti ogni pagina del PDF in un'immagine BMP." + }, + "pdfToTiff": { + "name": "PDF in TIFF", + "subtitle": "Converti ogni pagina del PDF in un'immagine TIFF." + }, + "pdfToGreyscale": { + "name": "PDF in Scala di Grigi", + "subtitle": "Converti tutti i colori in scala di grigi." + }, + "pdfToJson": { + "name": "PDF in JSON", + "subtitle": "Converti file PDF in formato JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Rendi un PDF ricercabile e copiabile." + }, + "alternateMix": { + "name": "Alterna e Riordina Pagine", + "subtitle": "Unisci PDF sostituendo le pagine di ogni file. Conserva i segnalibri." + }, + "addAttachments": { + "name": "Aggiungi Allegati", + "subtitle": "Incorpora uno o più file nel tuo PDF." + }, + "extractAttachments": { + "name": "Estrai Allegati", + "subtitle": "Estrai tutti i file incorporati dai PDF come archivio ZIP." + }, + "editAttachments": { + "name": "Modifica Allegati", + "subtitle": "Visualizza o rimuovi gli allegati nel tuo PDF." + }, + "dividePages": { + "name": "Dividi Pagine", + "subtitle": "Dividi le pagine orizzontalmente o verticalmente." + }, + "addBlankPage": { + "name": "Aggiungi Pagina Vuota", + "subtitle": "Inserisci una pagina vuota in qualsiasi punto del tuo PDF." + }, + "reversePages": { + "name": "Inverti Pagine", + "subtitle": "Inverti l'ordine di tutte le pagine del documento." + }, + "rotatePdf": { + "name": "Ruota PDF", + "subtitle": "Ruota le pagine per multipli di 90 gradi." + }, + "rotateCustom": { + "name": "Ruota di Gradi Personalizzati", + "subtitle": "Ruota le pagine di un angolo personalizzato." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Disponi più pagine su un unico foglio." + }, + "combineToSinglePage": { + "name": "Combina in Una Pagina", + "subtitle": "Unisci tutte le pagine in un'unica pagina continua." + }, + "viewMetadata": { + "name": "Visualizza Metadati", + "subtitle": "Ispeziona le proprietà nascoste del tuo PDF." + }, + "editMetadata": { + "name": "Modifica Metadati", + "subtitle": "Modifica autore, titolo e altre proprietà." + }, + "pdfsToZip": { + "name": "PDF in ZIP", + "subtitle": "Raggruppa più file PDF in un archivio ZIP." + }, + "comparePdfs": { + "name": "Confronta PDF", + "subtitle": "Confronta due PDF fianco a fianco." + }, + "posterizePdf": { + "name": "Posterizza PDF", + "subtitle": "Dividi una pagina grande in più pagine più piccole." + }, + "fixPageSize": { + "name": "Correggi Dimensione Pagina", + "subtitle": "Uniforma tutte le pagine a una dimensione standard." + }, + "linearizePdf": { + "name": "Linearizza PDF", + "subtitle": "Ottimizza il PDF per una visualizzazione web più veloce." + }, + "pageDimensions": { + "name": "Dimensioni Pagina", + "subtitle": "Analizza dimensione, orientamento e unità delle pagine." + }, + "removeRestrictions": { + "name": "Rimuovi Restrizioni", + "subtitle": "Rimuovi la protezione tramite password e le restrizioni di sicurezza associate ai PDF firmati digitalmente." + }, + "repairPdf": { + "name": "Ripara PDF", + "subtitle": "Recupera i dati da file PDF corrotti o danneggiati." + }, + "encryptPdf": { + "name": "Cripta PDF", + "subtitle": "Proteggi il tuo PDF aggiungendo una password." + }, + "sanitizePdf": { + "name": "Sanitizza PDF", + "subtitle": "Rimuovi metadati, annotazioni, script e altro." + }, + "decryptPdf": { + "name": "Decrittografa PDF", + "subtitle": "Sblocca il PDF rimuovendo la protezione tramite password." + }, + "flattenPdf": { + "name": "Appiattisci PDF", + "subtitle": "Rendi i campi dei moduli e le annotazioni non modificabili." + }, + "removeMetadata": { + "name": "Rimuovi metadati", + "subtitle": "Rimuovi i dati nascosti dal tuo PDF." + }, + "changePermissions": { + "name": "Modifica permessi", + "subtitle": "Imposta o modifica i permessi utente su un PDF." + }, + "odtToPdf": { + "name": "ODT in PDF", + "subtitle": "Converti file OpenDocument Text in formato PDF. Supporta più file.", + "acceptedFormats": "File ODT", + "convertButton": "Converti in PDF" + }, + "csvToPdf": { + "name": "CSV in PDF", + "subtitle": "Converti file di foglio di calcolo CSV in formato PDF. Supporta più file.", + "acceptedFormats": "File CSV", + "convertButton": "Converti in PDF" + }, + "rtfToPdf": { + "name": "RTF in PDF", + "subtitle": "Converti documenti Rich Text Format in PDF. Supporta più file.", + "acceptedFormats": "File RTF", + "convertButton": "Converti in PDF" + }, + "wordToPdf": { + "name": "Word in PDF", + "subtitle": "Converti documenti Word (DOCX, DOC, ODT, RTF) in formato PDF. Supporta più file.", + "acceptedFormats": "File DOCX, DOC, ODT, RTF", + "convertButton": "Converti in PDF" + }, + "excelToPdf": { + "name": "Excel in PDF", + "subtitle": "Converti fogli Excel (XLSX, XLS, ODS, CSV) in formato PDF. Supporta più file.", + "acceptedFormats": "File XLSX, XLS, ODS, CSV", + "convertButton": "Converti in PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint in PDF", + "subtitle": "Converti presentazioni PowerPoint (PPTX, PPT, ODP) in formato PDF. Supporta più file.", + "acceptedFormats": "File PPTX, PPT, ODP", + "convertButton": "Converti in PDF" + }, + "markdownToPdf": { + "name": "Markdown in PDF", + "subtitle": "Scrivi o incolla Markdown ed esportalo come un PDF ben formattato.", + "paneMarkdown": "Markdown", + "panePreview": "Anteprima", + "btnUpload": "Carica", + "btnSyncScroll": "Sincronizza scorrimento", + "btnSettings": "Impostazioni", + "btnExportPdf": "Esporta PDF", + "settingsTitle": "Impostazioni Markdown", + "settingsPreset": "Preset", + "presetDefault": "Predefinito (simile a GFM)", + "presetCommonmark": "CommonMark (rigoroso)", + "presetZero": "Minimale (nessuna funzionalità)", + "settingsOptions": "Opzioni Markdown", + "optAllowHtml": "Consenti tag HTML", + "optBreaks": "Converti nuove righe in
", + "optLinkify": "Converti automaticamente gli URL in link", + "optTypographer": "Tipografia (virgolette intelligenti, ecc.)" + }, + "pdfBooklet": { + "name": "Opuscolo PDF", + "subtitle": "Riorganizza le pagine per la stampa di opuscoli fronte-retro. Piega e pinza per creare un opuscolo.", + "howItWorks": "Come funziona:", + "step1": "Carica un file PDF.", + "step2": "Le pagine saranno riorganizzate in ordine per opuscolo.", + "step3": "Stampa fronte-retro, capovolgi sul lato corto, piega e pinza.", + "paperSize": "Formato carta", + "orientation": "Orientamento", + "portrait": "Ritratto", + "landscape": "Paesaggio", + "pagesPerSheet": "Pagine per foglio", + "createBooklet": "Crea opuscolo", + "processing": "Elaborazione...", + "pageCount": "Il conteggio delle pagine verrà arrotondato ad un multiplo di 4 se necessario." + }, + "xpsToPdf": { + "name": "XPS in PDF", + "subtitle": "Converti documenti XPS/OXPS in formato PDF. Supporta più file.", + "acceptedFormats": "File XPS, OXPS", + "convertButton": "Converti in PDF" + }, + "mobiToPdf": { + "name": "MOBI in PDF", + "subtitle": "Converti e-book MOBI in formato PDF. Supporta più file.", + "acceptedFormats": "File MOBI", + "convertButton": "Converti in PDF" + }, + "epubToPdf": { + "name": "EPUB in PDF", + "subtitle": "Converti e-book EPUB in formato PDF. Supporta più file.", + "acceptedFormats": "File EPUB", + "convertButton": "Converti in PDF" + }, + "fb2ToPdf": { + "name": "FB2 in PDF", + "subtitle": "Converti e-book FictionBook (FB2) in formato PDF. Supporta più file.", + "acceptedFormats": "File FB2", + "convertButton": "Converti in PDF" + }, + "cbzToPdf": { + "name": "CBZ in PDF", + "subtitle": "Converti archivi di fumetti (CBZ/CBR) in formato PDF. Supporta più file.", + "acceptedFormats": "File CBZ, CBR", + "convertButton": "Converti in PDF" + }, + "wpdToPdf": { + "name": "WPD in PDF", + "subtitle": "Converti documenti WordPerfect (WPD) in formato PDF. Supporta più file.", + "acceptedFormats": "File WPD", + "convertButton": "Converti in PDF" + }, + "wpsToPdf": { + "name": "WPS in PDF", + "subtitle": "Converti documenti WPS Office in formato PDF. Supporta più file.", + "acceptedFormats": "File WPS", + "convertButton": "Converti in PDF" + }, + "xmlToPdf": { + "name": "XML in PDF", + "subtitle": "Converti documenti XML in formato PDF. Supporta più file.", + "acceptedFormats": "File XML", + "convertButton": "Converti in PDF" + }, + "pagesToPdf": { + "name": "Pages in PDF", + "subtitle": "Converti documenti Apple Pages in formato PDF. Supporta più file.", + "acceptedFormats": "File Pages", + "convertButton": "Converti in PDF" + }, + "odgToPdf": { + "name": "ODG in PDF", + "subtitle": "Converti OpenDocument Graphics (ODG) in formato PDF. Supporta più file.", + "acceptedFormats": "File ODG", + "convertButton": "Converti in PDF" + }, + "odsToPdf": { + "name": "ODS in PDF", + "subtitle": "Converti fogli OpenDocument (ODS) in formato PDF. Supporta più file.", + "acceptedFormats": "File ODS", + "convertButton": "Converti in PDF" + }, + "odpToPdf": { + "name": "ODP in PDF", + "subtitle": "Converti presentazioni OpenDocument (ODP) in formato PDF. Supporta più file.", + "acceptedFormats": "File ODP", + "convertButton": "Converti in PDF" + }, + "pubToPdf": { + "name": "PUB in PDF", + "subtitle": "Converti file Microsoft Publisher (PUB) in formato PDF. Supporta più file.", + "acceptedFormats": "File PUB", + "convertButton": "Converti in PDF" + }, + "vsdToPdf": { + "name": "VSD in PDF", + "subtitle": "Converti file Microsoft Visio (VSD, VSDX) in formato PDF. Supporta più file.", + "acceptedFormats": "File VSD, VSDX", + "convertButton": "Converti in PDF" + }, + "psdToPdf": { + "name": "PSD in PDF", + "subtitle": "Converti file Adobe Photoshop (PSD) in formato PDF. Supporta più file.", + "acceptedFormats": "File PSD", + "convertButton": "Converti in PDF" + }, + "pdfToSvg": { + "name": "PDF in SVG", + "subtitle": "Converti ogni pagina di un file PDF in un'immagine vettoriale scalabile (SVG) per qualità perfetta a qualsiasi dimensione." + }, + "extractTables": { + "name": "Estrai tabelle PDF", + "subtitle": "Estrai le tabelle dai file PDF ed esportale come CSV, JSON o Markdown." + }, + "pdfToCsv": { + "name": "PDF in CSV", + "subtitle": "Estrai tabelle dai PDF e convertili in formato CSV." + }, + "pdfToExcel": { + "name": "PDF in Excel", + "subtitle": "Estrai tabelle dai PDF e convertili in Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF in Testo", + "subtitle": "Estrai il testo dai file PDF e salvalo come testo semplice (.txt). Supporta più file.", + "note": "Questo strumento funziona SOLO con PDF creati digitalmente. Per documenti scansionati o basati su immagini, usa invece il nostro strumento OCR PDF.", + "convertButton": "Estrai testo" + }, + "emailToPdf": { + "name": "Email in PDF", + "subtitle": "Converti file email (EML, MSG) in formato PDF. Supporta esportazioni Outlook e formati email standard.", + "acceptedFormats": "File EML, MSG", + "convertButton": "Converti in PDF" + } +} diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 34bc151f3..8467d2559 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -1,282 +1,288 @@ { - "categories": { - "popularTools": "Popüler Araçlar", - "editAnnotate": "Düzenle & Açıklama Ekle", - "convertToPdf": "PDF'ye Dönüştür", - "convertFromPdf": "PDF'den Dönüştür", - "organizeManage": "Düzenle & Yönet", - "optimizeRepair": "Optimize Et & Onar", - "securePdf": "PDF Güvenliği" - }, - "pdfMultiTool": { - "name": "PDF Çoklu Araç", - "subtitle": "Birleştir, Böl, Düzenle, Sil, Döndür, Boş Sayfa Ekle, Çıkar ve Çoğalt işlemlerini tek bir arayüzde yapın." - }, - "mergePdf": { - "name": "PDF Birleştir", - "subtitle": "Birden fazla PDF'yi tek bir dosyada birleştirin. Yer imlerini korur." - }, - "splitPdf": { - "name": "PDF Böl", - "subtitle": "Sayfa aralığını yeni bir PDF olarak çıkarın." - }, - "compressPdf": { - "name": "PDF Sıkıştır", - "subtitle": "PDF dosya boyutunu küçültün." - }, - "pdfEditor": { - "name": "PDF Düzenleyici", - "subtitle": "Açıklama ekleyin, vurgulayın, düzenleyin, yorum yapın, şekil/resim ekleyin, arama yapın ve PDF'leri görüntüleyin." - }, - "jpgToPdf": { - "name": "JPG'den PDF'ye", - "subtitle": "Bir veya daha fazla JPG görselinden PDF oluşturun." - }, - "signPdf": { - "name": "PDF İmzala", - "subtitle": "İmzanızı çizin, yazın veya yükleyin." - }, - "cropPdf": { - "name": "PDF Kırp", - "subtitle": "PDF'nizdeki her sayfanın kenar boşluklarını kırpın." - }, - "extractPages": { - "name": "Sayfaları Çıkar", - "subtitle": "Seçili sayfaları yeni dosyalar olarak kaydedin." - }, - "duplicateOrganize": { - "name": "Çoğalt & Düzenle", - "subtitle": "Sayfaları çoğaltın, yeniden sıralayın ve silin." - }, - "deletePages": { - "name": "Sayfaları Sil", - "subtitle": "Belgenizden belirli sayfaları kaldırın." - }, - "editBookmarks": { - "name": "Yer İşaretlerini Düzenle", - "subtitle": "PDF yer imlerini ekleyin, düzenleyin, içe aktarın, silin ve çıkarın." - }, - "tableOfContents": { - "name": "İçindekiler", - "subtitle": "PDF yer imlerinden bir içindekiler sayfası oluşturun." - }, - "pageNumbers": { - "name": "Sayfa Numaraları", - "subtitle": "Belgenize sayfa numaraları ekleyin." - }, - "addWatermark": { - "name": "Filigran Ekle", - "subtitle": "PDF sayfalarınızın üzerine metin veya görsel damgası ekleyin." - }, - "headerFooter": { - "name": "Üst Bilgi & Alt Bilgi", - "subtitle": "Sayfaların üst ve alt kısmına metin ekleyin." - }, - "invertColors": { - "name": "Renkleri Ters Çevir", - "subtitle": "PDF'niz için \"karanlık mod\" sürümü oluşturun." - }, - "backgroundColor": { - "name": "Arka Plan Rengi", - "subtitle": "PDF'nizin arka plan rengini değiştirin." - }, - "changeTextColor": { - "name": "Metin Rengini Değiştir", - "subtitle": "PDF'nizdeki metnin rengini değiştirin." - }, - "addStamps": { - "name": "Damga Ekle", - "subtitle": "Açıklama araç çubuğunu kullanarak PDF'nize damga ekleyin.", - "usernameLabel": "Kullanıcı Adı", - "usernamePlaceholder": "Adınızı girin (damgalar için)", - "usernameHint": "Bu isim oluşturduğunuz damgalarda görünecektir." - }, - "removeAnnotations": { - "name": "Açıklamaları Kaldır", - "subtitle": "Yorumları, vurguları ve bağlantıları kaldırın." - }, - "pdfFormFiller": { - "name": "PDF Form Doldurucu", - "subtitle": "Formları doğrudan tarayıcıda doldurun. XFA formlarını da destekler." - }, - "createPdfForm": { - "name": "PDF Formu Oluştur", - "subtitle": "Sürükle-bırak metin alanları ile doldurulabilir PDF formları oluşturun." - }, - "removeBlankPages": { - "name": "Boş Sayfaları Kaldır", - "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin." - }, - "imageToPdf": { - "name": "Görselden PDF'ye", - "subtitle": "JPG, PNG, WebP, BMP, TIFF, SVG, HEIC formatlarını PDF'ye dönüştürün." - }, - "pngToPdf": { - "name": "PNG'den PDF'ye", - "subtitle": "Bir veya daha fazla PNG görselinden PDF oluşturun." - }, - "webpToPdf": { - "name": "WebP'den PDF'ye", - "subtitle": "Bir veya daha fazla WebP görselinden PDF oluşturun." - }, - "svgToPdf": { - "name": "SVG'den PDF'ye", - "subtitle": "Bir veya daha fazla SVG görselinden PDF oluşturun." - }, - "bmpToPdf": { - "name": "BMP'den PDF'ye", - "subtitle": "Bir veya daha fazla BMP görselinden PDF oluşturun." - }, - "heicToPdf": { - "name": "HEIC'den PDF'ye", - "subtitle": "Bir veya daha fazla HEIC görselinden PDF oluşturun." - }, - "tiffToPdf": { - "name": "TIFF'den PDF'ye", - "subtitle": "Bir veya daha fazla TIFF görselinden PDF oluşturun." - }, - "textToPdf": { - "name": "Metinden PDF'ye", - "subtitle": "Düz metin dosyasını PDF'ye dönüştürün." - }, - "jsonToPdf": { - "name": "JSON'dan PDF'ye", - "subtitle": "JSON dosyalarını PDF formatına dönüştürün." - }, - "pdfToJpg": { - "name": "PDF'den JPG'ye", - "subtitle": "Her PDF sayfasını JPG görseline dönüştürün." - }, - "pdfToPng": { - "name": "PDF'den PNG'ye", - "subtitle": "Her PDF sayfasını PNG görseline dönüştürün." - }, - "pdfToWebp": { - "name": "PDF'den WebP'ye", - "subtitle": "Her PDF sayfasını WebP görseline dönüştürün." - }, - "pdfToBmp": { - "name": "PDF'den BMP'ye", - "subtitle": "Her PDF sayfasını BMP görseline dönüştürün." - }, - "pdfToTiff": { - "name": "PDF'den TIFF'e", - "subtitle": "Her PDF sayfasını TIFF görseline dönüştürün." - }, - "pdfToGreyscale": { - "name": "PDF'yi Gri Tonlamaya Çevir", - "subtitle": "Tüm renkleri siyah beyaza çevirin." - }, - "pdfToJson": { - "name": "PDF'den JSON'a", - "subtitle": "PDF dosyalarını JSON formatına dönüştürün." - }, - "ocrPdf": { - "name": "PDF'de OCR", - "subtitle": "PDF'yi aranabilir ve kopyalanabilir hale getirin." - }, - "alternateMix": { - "name": "Sayfaları Karıştır & Birleştir", - "subtitle": "PDF'leri her birinden sayfaları sırayla birleştirin. Yer imlerini korur." - }, - "addAttachments": { - "name": "Ek Dosya Ekle", - "subtitle": "PDF'nize bir veya daha fazla dosya ekleyin." - }, - "extractAttachments": { - "name": "Ek Dosyaları Çıkar", - "subtitle": "PDF'lerden tüm gömülü dosyaları ZIP olarak çıkarın." - }, - "editAttachments": { - "name": "Ek Dosyaları Düzenle", - "subtitle": "PDF'nizdeki ek dosyaları görüntüleyin veya kaldırın." - }, - "dividePages": { - "name": "Sayfaları Böl", - "subtitle": "Sayfaları yatay veya dikey olarak bölün." - }, - "addBlankPage": { - "name": "Boş Sayfa Ekle", - "subtitle": "PDF'nize herhangi bir yerine boş sayfa ekleyin." - }, - "reversePages": { - "name": "Sayfaları Ters Çevir", - "subtitle": "Belgenizdeki tüm sayfaların sırasını tersine çevirin." - }, - "rotatePdf": { - "name": "PDF'yi Döndür", - "subtitle": "Sayfaları 90 derecelik artışlarla döndürün." - }, - "nUpPdf": { - "name": "N'li PDF", - "subtitle": "Birden fazla sayfayı tek bir sayfaya yerleştirin." - }, - "combineToSinglePage": { - "name": "Tek Sayfada Birleştir", - "subtitle": "Tüm sayfaları tek bir sürekli kaydırılabilir sayfada birleştirin." - }, - "viewMetadata": { - "name": "Üst Veriyi Görüntüle", - "subtitle": "PDF'nizin gizli özelliklerini inceleyin." - }, - "editMetadata": { - "name": "Üst Veriyi Düzenle", - "subtitle": "Yazar, başlık ve diğer özellikleri değiştirin." - }, - "pdfsToZip": { - "name": "PDF'leri ZIP Yap", - "subtitle": "Birden fazla PDF dosyasını bir ZIP arşivinde paketleyin." - }, - "comparePdfs": { - "name": "PDF'leri Karşılaştır", - "subtitle": "İki PDF'yi yan yana karşılaştırın." - }, - "posterizePdf": { - "name": "PDF'yi Posta Boyutuna Böl", - "subtitle": "Büyük bir sayfayı birden fazla küçük sayfaya bölün." - }, - "fixPageSize": { - "name": "Sayfa Boyutunu Düzelt", - "subtitle": "Tüm sayfaları standart bir boyuta getirin." - }, - "linearizePdf": { - "name": "PDF'yi Doğrusallaştır", - "subtitle": "Hızlı web görüntüleme için PDF'yi optimize edin." - }, - "pageDimensions": { - "name": "Sayfa Boyutları", - "subtitle": "Sayfa boyutunu, yönlendirmeyi ve birimleri analiz edin." - }, - "removeRestrictions": { - "name": "Kısıtlamaları Kaldır", - "subtitle": "Dijital olarak imzalanmış PDF dosyalarıyla ilişkili şifre korumasını ve güvenlik kısıtlamalarını kaldırın." - }, - "repairPdf": { - "name": "PDF'yi Onar", - "subtitle": "Bozulmuş veya hasarlı PDF dosyalarından veri kurtarın." - }, - "encryptPdf": { - "name": "PDF'yi Şifrele", - "subtitle": "PDF'nizi şifre ekleyerek koruyun." - }, - "sanitizePdf": { - "name": "PDF'yi Temizle", - "subtitle": "Üst verileri, açıklamaları, betikleri ve daha fazlasını kaldırın." - }, - "decryptPdf": { - "name": "PDF'nin Şifresini Çöz", - "subtitle": "Şifre korumasını kaldırarak PDF'nin kilidini açın." - }, - "flattenPdf": { - "name": "PDF'yi Düzleştir", - "subtitle": "Form alanlarını ve açıklamaları düzenlenemez hale getirin." - }, - "removeMetadata": { - "name": "Üst Veriyi Kaldır", - "subtitle": "PDF'nizdeki gizli verileri temizleyin." - }, - "changePermissions": { - "name": "İzinleri Değiştir", - "subtitle": "Bir PDF üzerindeki kullanıcı izinlerini ayarlayın veya değiştirin." - } -} \ No newline at end of file + "categories": { + "popularTools": "Popüler Araçlar", + "editAnnotate": "Düzenle & Açıklama Ekle", + "convertToPdf": "PDF'ye Dönüştür", + "convertFromPdf": "PDF'den Dönüştür", + "organizeManage": "Düzenle & Yönet", + "optimizeRepair": "Optimize Et & Onar", + "securePdf": "PDF Güvenliği" + }, + "pdfMultiTool": { + "name": "PDF Çoklu Araç", + "subtitle": "Birleştir, Böl, Düzenle, Sil, Döndür, Boş Sayfa Ekle, Çıkar ve Çoğalt işlemlerini tek bir arayüzde yapın." + }, + "mergePdf": { + "name": "PDF Birleştir", + "subtitle": "Birden fazla PDF'yi tek bir dosyada birleştirin. Yer imlerini korur." + }, + "splitPdf": { + "name": "PDF Böl", + "subtitle": "Sayfa aralığını yeni bir PDF olarak çıkarın." + }, + "compressPdf": { + "name": "PDF Sıkıştır", + "subtitle": "PDF dosya boyutunu küçültün." + }, + "pdfEditor": { + "name": "PDF Düzenleyici", + "subtitle": "Açıklama ekleyin, vurgulayın, düzenleyin, yorum yapın, şekil/resim ekleyin, arama yapın ve PDF'leri görüntüleyin." + }, + "jpgToPdf": { + "name": "JPG'den PDF'ye", + "subtitle": "Bir veya daha fazla JPG görselinden PDF oluşturun." + }, + "signPdf": { + "name": "PDF İmzala", + "subtitle": "İmzanızı çizin, yazın veya yükleyin." + }, + "cropPdf": { + "name": "PDF Kırp", + "subtitle": "PDF'nizdeki her sayfanın kenar boşluklarını kırpın." + }, + "extractPages": { + "name": "Sayfaları Çıkar", + "subtitle": "Seçili sayfaları yeni dosyalar olarak kaydedin." + }, + "duplicateOrganize": { + "name": "Çoğalt & Düzenle", + "subtitle": "Sayfaları çoğaltın, yeniden sıralayın ve silin." + }, + "deletePages": { + "name": "Sayfaları Sil", + "subtitle": "Belgenizden belirli sayfaları kaldırın." + }, + "editBookmarks": { + "name": "Yer İşaretlerini Düzenle", + "subtitle": "PDF yer imlerini ekleyin, düzenleyin, içe aktarın, silin ve çıkarın." + }, + "tableOfContents": { + "name": "İçindekiler", + "subtitle": "PDF yer imlerinden bir içindekiler sayfası oluşturun." + }, + "pageNumbers": { + "name": "Sayfa Numaraları", + "subtitle": "Belgenize sayfa numaraları ekleyin." + }, + "addWatermark": { + "name": "Filigran Ekle", + "subtitle": "PDF sayfalarınızın üzerine metin veya görsel damgası ekleyin." + }, + "headerFooter": { + "name": "Üst Bilgi & Alt Bilgi", + "subtitle": "Sayfaların üst ve alt kısmına metin ekleyin." + }, + "invertColors": { + "name": "Renkleri Ters Çevir", + "subtitle": "PDF'niz için \"karanlık mod\" sürümü oluşturun." + }, + "backgroundColor": { + "name": "Arka Plan Rengi", + "subtitle": "PDF'nizin arka plan rengini değiştirin." + }, + "changeTextColor": { + "name": "Metin Rengini Değiştir", + "subtitle": "PDF'nizdeki metnin rengini değiştirin." + }, + "addStamps": { + "name": "Damga Ekle", + "subtitle": "Açıklama araç çubuğunu kullanarak PDF'nize damga ekleyin.", + "usernameLabel": "Kullanıcı Adı", + "usernamePlaceholder": "Adınızı girin (damgalar için)", + "usernameHint": "Bu isim oluşturduğunuz damgalarda görünecektir." + }, + "removeAnnotations": { + "name": "Açıklamaları Kaldır", + "subtitle": "Yorumları, vurguları ve bağlantıları kaldırın." + }, + "pdfFormFiller": { + "name": "PDF Form Doldurucu", + "subtitle": "Formları doğrudan tarayıcıda doldurun. XFA formlarını da destekler." + }, + "createPdfForm": { + "name": "PDF Formu Oluştur", + "subtitle": "Sürükle-bırak metin alanları ile doldurulabilir PDF formları oluşturun." + }, + "removeBlankPages": { + "name": "Boş Sayfaları Kaldır", + "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin." + }, + "imageToPdf": { + "name": "Görselden PDF'ye", + "subtitle": "JPG, PNG, WebP, BMP, TIFF, SVG, HEIC formatlarını PDF'ye dönüştürün." + }, + "pngToPdf": { + "name": "PNG'den PDF'ye", + "subtitle": "Bir veya daha fazla PNG görselinden PDF oluşturun." + }, + "webpToPdf": { + "name": "WebP'den PDF'ye", + "subtitle": "Bir veya daha fazla WebP görselinden PDF oluşturun." + }, + "svgToPdf": { + "name": "SVG'den PDF'ye", + "subtitle": "Bir veya daha fazla SVG görselinden PDF oluşturun." + }, + "bmpToPdf": { + "name": "BMP'den PDF'ye", + "subtitle": "Bir veya daha fazla BMP görselinden PDF oluşturun." + }, + "heicToPdf": { + "name": "HEIC'den PDF'ye", + "subtitle": "Bir veya daha fazla HEIC görselinden PDF oluşturun." + }, + "tiffToPdf": { + "name": "TIFF'den PDF'ye", + "subtitle": "Bir veya daha fazla TIFF görselinden PDF oluşturun." + }, + "textToPdf": { + "name": "Metinden PDF'ye", + "subtitle": "Düz metin dosyasını PDF'ye dönüştürün." + }, + "jsonToPdf": { + "name": "JSON'dan PDF'ye", + "subtitle": "JSON dosyalarını PDF formatına dönüştürün." + }, + "pdfToJpg": { + "name": "PDF'den JPG'ye", + "subtitle": "Her PDF sayfasını JPG görseline dönüştürün." + }, + "pdfToPng": { + "name": "PDF'den PNG'ye", + "subtitle": "Her PDF sayfasını PNG görseline dönüştürün." + }, + "pdfToWebp": { + "name": "PDF'den WebP'ye", + "subtitle": "Her PDF sayfasını WebP görseline dönüştürün." + }, + "pdfToBmp": { + "name": "PDF'den BMP'ye", + "subtitle": "Her PDF sayfasını BMP görseline dönüştürün." + }, + "pdfToTiff": { + "name": "PDF'den TIFF'e", + "subtitle": "Her PDF sayfasını TIFF görseline dönüştürün." + }, + "pdfToGreyscale": { + "name": "PDF'yi Gri Tonlamaya Çevir", + "subtitle": "Tüm renkleri siyah beyaza çevirin." + }, + "pdfToJson": { + "name": "PDF'den JSON'a", + "subtitle": "PDF dosyalarını JSON formatına dönüştürün." + }, + "ocrPdf": { + "name": "PDF'de OCR", + "subtitle": "PDF'yi aranabilir ve kopyalanabilir hale getirin." + }, + "alternateMix": { + "name": "Sayfaları Karıştır & Birleştir", + "subtitle": "PDF'leri her birinden sayfaları sırayla birleştirin. Yer imlerini korur." + }, + "addAttachments": { + "name": "Ek Dosya Ekle", + "subtitle": "PDF'nize bir veya daha fazla dosya ekleyin." + }, + "extractAttachments": { + "name": "Ek Dosyaları Çıkar", + "subtitle": "PDF'lerden tüm gömülü dosyaları ZIP olarak çıkarın." + }, + "editAttachments": { + "name": "Ek Dosyaları Düzenle", + "subtitle": "PDF'nizdeki ek dosyaları görüntüleyin veya kaldırın." + }, + "dividePages": { + "name": "Sayfaları Böl", + "subtitle": "Sayfaları yatay veya dikey olarak bölün." + }, + "addBlankPage": { + "name": "Boş Sayfa Ekle", + "subtitle": "PDF'nize herhangi bir yerine boş sayfa ekleyin." + }, + "reversePages": { + "name": "Sayfaları Ters Çevir", + "subtitle": "Belgenizdeki tüm sayfaların sırasını tersine çevirin." + }, + "rotatePdf": { + "name": "PDF'yi Döndür", + "subtitle": "Sayfaları 90 derecelik artışlarla döndürün." + }, + "nUpPdf": { + "name": "N'li PDF", + "subtitle": "Birden fazla sayfayı tek bir sayfaya yerleştirin." + }, + "combineToSinglePage": { + "name": "Tek Sayfada Birleştir", + "subtitle": "Tüm sayfaları tek bir sürekli kaydırılabilir sayfada birleştirin." + }, + "viewMetadata": { + "name": "Üst Veriyi Görüntüle", + "subtitle": "PDF'nizin gizli özelliklerini inceleyin." + }, + "editMetadata": { + "name": "Üst Veriyi Düzenle", + "subtitle": "Yazar, başlık ve diğer özellikleri değiştirin." + }, + "pdfsToZip": { + "name": "PDF'leri ZIP Yap", + "subtitle": "Birden fazla PDF dosyasını bir ZIP arşivinde paketleyin." + }, + "comparePdfs": { + "name": "PDF'leri Karşılaştır", + "subtitle": "İki PDF'yi yan yana karşılaştırın." + }, + "posterizePdf": { + "name": "PDF'yi Posta Boyutuna Böl", + "subtitle": "Büyük bir sayfayı birden fazla küçük sayfaya bölün." + }, + "fixPageSize": { + "name": "Sayfa Boyutunu Düzelt", + "subtitle": "Tüm sayfaları standart bir boyuta getirin." + }, + "linearizePdf": { + "name": "PDF'yi Doğrusallaştır", + "subtitle": "Hızlı web görüntüleme için PDF'yi optimize edin." + }, + "pageDimensions": { + "name": "Sayfa Boyutları", + "subtitle": "Sayfa boyutunu, yönlendirmeyi ve birimleri analiz edin." + }, + "removeRestrictions": { + "name": "Kısıtlamaları Kaldır", + "subtitle": "Dijital olarak imzalanmış PDF dosyalarıyla ilişkili şifre korumasını ve güvenlik kısıtlamalarını kaldırın." + }, + "repairPdf": { + "name": "PDF'yi Onar", + "subtitle": "Bozulmuş veya hasarlı PDF dosyalarından veri kurtarın." + }, + "encryptPdf": { + "name": "PDF'yi Şifrele", + "subtitle": "PDF'nizi şifre ekleyerek koruyun." + }, + "sanitizePdf": { + "name": "PDF'yi Temizle", + "subtitle": "Üst verileri, açıklamaları, betikleri ve daha fazlasını kaldırın." + }, + "decryptPdf": { + "name": "PDF'nin Şifresini Çöz", + "subtitle": "Şifre korumasını kaldırarak PDF'nin kilidini açın." + }, + "flattenPdf": { + "name": "PDF'yi Düzleştir", + "subtitle": "Form alanlarını ve açıklamaları düzenlenemez hale getirin." + }, + "removeMetadata": { + "name": "Üst Veriyi Kaldır", + "subtitle": "PDF'nizdeki gizli verileri temizleyin." + }, + "changePermissions": { + "name": "İzinleri Değiştir", + "subtitle": "Bir PDF üzerindeki kullanıcı izinlerini ayarlayın veya değiştirin." + }, + "emailToPdf": { + "name": "E-posta'dan PDF'ye", + "subtitle": "E-posta dosyalarını (EML, MSG) PDF formatına dönüştürün. Outlook dışa aktarmalarını ve standart e-posta formatlarını destekler.", + "acceptedFormats": "EML, MSG Dosyaları", + "convertButton": "PDF'ye Dönüştür" + } +} diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index fe6dff4b3..21df99136 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -1,519 +1,525 @@ { - "categories": { - "popularTools": "Công cụ phổ biến", - "editAnnotate": "Chỉnh sửa & Ghi chú", - "convertToPdf": "Chuyển đổi sang PDF", - "convertFromPdf": "Chuyển đổi từ PDF", - "organizeManage": "Sắp xếp & Quản lý", - "optimizeRepair": "Tối ưu hóa & Sửa chữa", - "securePdf": "Bảo mật PDF" - }, - "pdfMultiTool": { - "name": "Công cụ đa năng PDF", - "subtitle": "Gộp, Chia, Sắp xếp, Xóa, Xoay, Thêm trang trống, Trích xuất và Nhân đôi trong một giao diện thống nhất." - }, - "mergePdf": { - "name": "Gộp PDF", - "subtitle": "Kết hợp nhiều PDF thành một tệp. Giữ nguyên Bookmark." - }, - "splitPdf": { - "name": "Chia PDF", - "subtitle": "Trích xuất một phạm vi trang thành PDF mới." - }, - "compressPdf": { - "name": "Nén PDF", - "subtitle": "Giảm kích thước tệp PDF của bạn.", - "algorithmLabel": "Thuật toán nén", - "condense": "Condense (Khuyến nghị)", - "photon": "Photon (Dành cho PDF nhiều ảnh)", - "condenseInfo": "Condense sử dụng nén nâng cao: loại bỏ dữ liệu thừa, tối ưu hóa hình ảnh, gọn phông chữ. Phù hợp với hầu hết PDF.", - "photonInfo": "Photon chuyển đổi trang thành hình ảnh. Dùng cho PDF nhiều ảnh/quét.", - "photonWarning": "Cảnh báo: Văn bản sẽ không thể chọn được và liên kết sẽ không hoạt động.", - "levelLabel": "Mức độ nén", - "light": "Nhẹ (Giữ chất lượng)", - "balanced": "Cân bằng (Khuyến nghị)", - "aggressive": "Mạnh (Tệp nhỏ hơn)", - "extreme": "Cực đoan (Nén tối đa)", - "grayscale": "Chuyển sang thang xám", - "grayscaleHint": "Giảm kích thước tệp bằng cách loại bỏ thông tin màu", - "customSettings": "Cài đặt tùy chỉnh", - "customSettingsHint": "Tinh chỉnh các thông số nén:", - "outputQuality": "Chất lượng đầu ra", - "resizeImagesTo": "Thay đổi kích thước ảnh thành", - "onlyProcessAbove": "Chỉ xử lý khi trên", - "removeMetadata": "Xóa siêu dữ liệu", - "subsetFonts": "Gọn phông chữ (xóa ký tự không dùng)", - "removeThumbnails": "Xóa hình thu nhỏ nhúng", - "compressButton": "Nén PDF" - }, - "pdfEditor": { - "name": "Trình chỉnh sửa PDF", - "subtitle": "Ghi chú, tô sáng, chỉnh sửa, bình luận, thêm hình dạng/hình ảnh, tìm kiếm và xem PDF." - }, - "jpgToPdf": { - "name": "JPG sang PDF", - "subtitle": "Tạo PDF từ hình ảnh JPG, JPEG và JPEG2000 (JP2/JPX)." - }, - "signPdf": { - "name": "Ký PDF", - "subtitle": "Vẽ, gõ hoặc tải lên chữ ký của bạn." - }, - "cropPdf": { - "name": "Cắt PDF", - "subtitle": "Cắt lề của mọi trang trong PDF của bạn." - }, - "extractPages": { - "name": "Trích xuất trang", - "subtitle": "Lưu một lựa chọn trang dưới dạng tệp mới." - }, - "duplicateOrganize": { - "name": "Nhân đôi & Sắp xếp", - "subtitle": "Nhân đôi, sắp xếp lại và xóa trang." - }, - "deletePages": { - "name": "Xóa trang", - "subtitle": "Xóa các trang cụ thể khỏi tài liệu của bạn." - }, - "editBookmarks": { - "name": "Chỉnh sửa Bookmark", - "subtitle": "Thêm, chỉnh sửa, nhập, xóa và trích xuất bookmark PDF." - }, - "tableOfContents": { - "name": "Mục lục", - "subtitle": "Tạo trang mục lục từ bookmark PDF." - }, - "pageNumbers": { - "name": "Số trang", - "subtitle": "Chèn số trang vào tài liệu của bạn." - }, - "addWatermark": { - "name": "Thêm Watermark", - "subtitle": "Đóng dấu văn bản hoặc hình ảnh lên các trang PDF của bạn." - }, - "headerFooter": { - "name": "Đầu trang & Chân trang", - "subtitle": "Thêm văn bản vào đầu và cuối trang." - }, - "invertColors": { - "name": "Đảo ngược màu", - "subtitle": "Tạo phiên bản \"chế độ tối\" cho PDF của bạn." - }, - "backgroundColor": { - "name": "Màu nền", - "subtitle": "Thay đổi màu nền của PDF của bạn." - }, - "changeTextColor": { - "name": "Thay đổi màu văn bản", - "subtitle": "Thay đổi màu văn bản trong PDF của bạn." - }, - "addStamps": { - "name": "Thêm tem", - "subtitle": "Thêm tem hình ảnh vào PDF của bạn bằng thanh công cụ ghi chú.", - "usernameLabel": "Tên người dùng tem", - "usernamePlaceholder": "Nhập tên của bạn (cho tem)", - "usernameHint": "Tên này sẽ xuất hiện trên các tem bạn tạo." - }, - "removeAnnotations": { - "name": "Xóa ghi chú", - "subtitle": "Loại bỏ bình luận, tô sáng và liên kết." - }, - "pdfFormFiller": { - "name": "Điền form PDF", - "subtitle": "Điền form trực tiếp trong trình duyệt. Cũng hỗ trợ form XFA." - }, - "createPdfForm": { - "name": "Tạo form PDF", - "subtitle": "Tạo form PDF có thể điền với các trường văn bản kéo và thả." - }, - "removeBlankPages": { - "name": "Xóa trang trống", - "subtitle": "Tự động phát hiện và xóa trang trống." - }, - "imageToPdf": { - "name": "Hình ảnh sang PDF", - "subtitle": "Chuyển đổi JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP sang PDF." - }, - "pngToPdf": { - "name": "PNG sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh PNG." - }, - "webpToPdf": { - "name": "WebP sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh WebP." - }, - "svgToPdf": { - "name": "SVG sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh SVG." - }, - "bmpToPdf": { - "name": "BMP sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh BMP." - }, - "heicToPdf": { - "name": "HEIC sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh HEIC." - }, - "tiffToPdf": { - "name": "TIFF sang PDF", - "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh TIFF." - }, - "textToPdf": { - "name": "Văn bản sang PDF", - "subtitle": "Chuyển đổi tệp văn bản thuần túy thành PDF." - }, - "jsonToPdf": { - "name": "JSON sang PDF", - "subtitle": "Chuyển đổi tệp JSON sang định dạng PDF." - }, - "pdfToJpg": { - "name": "PDF sang JPG", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh JPG." - }, - "pdfToPng": { - "name": "PDF sang PNG", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh PNG." - }, - "pdfToWebp": { - "name": "PDF sang WebP", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh WebP." - }, - "pdfToBmp": { - "name": "PDF sang BMP", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh BMP." - }, - "pdfToTiff": { - "name": "PDF sang TIFF", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh TIFF." - }, - "pdfToGreyscale": { - "name": "PDF sang thang xám", - "subtitle": "Chuyển đổi tất cả màu sắc sang đen trắng." - }, - "pdfToJson": { - "name": "PDF sang JSON", - "subtitle": "Chuyển đổi tệp PDF sang định dạng JSON." - }, - "ocrPdf": { - "name": "OCR PDF", - "subtitle": "Làm cho PDF có thể tìm kiếm và sao chép được." - }, - "alternateMix": { - "name": "Xen kẽ & Trộn trang", - "subtitle": "Gộp PDF bằng cách xen kẽ trang từ mỗi PDF. Giữ nguyên Bookmark." - }, - "addAttachments": { - "name": "Thêm tệp đính kèm", - "subtitle": "Nhúng một hoặc nhiều tệp vào PDF của bạn." - }, - "extractAttachments": { - "name": "Trích xuất tệp đính kèm", - "subtitle": "Trích xuất tất cả tệp được nhúng từ PDF thành ZIP." - }, - "editAttachments": { - "name": "Chỉnh sửa tệp đính kèm", - "subtitle": "Xem hoặc xóa tệp đính kèm trong PDF của bạn." - }, - "dividePages": { - "name": "Chia trang", - "subtitle": "Chia trang theo chiều ngang hoặc chiều dọc." - }, - "addBlankPage": { - "name": "Thêm trang trống", - "subtitle": "Chèn trang trống ở bất kỳ đâu trong PDF của bạn." - }, - "reversePages": { - "name": "Đảo ngược trang", - "subtitle": "Lật ngược thứ tự tất cả các trang trong tài liệu của bạn." - }, - "rotatePdf": { - "name": "Xoay PDF", - "subtitle": "Xoay trang theo bội số 90 độ." - }, - "rotateCustom": { - "name": "Xoay theo độ tùy chỉnh", - "subtitle": "Xoay trang theo bất kỳ góc độ tùy chỉnh nào." - }, - "nUpPdf": { - "name": "N-Up PDF", - "subtitle": "Sắp xếp nhiều trang lên một tờ." - }, - "combineToSinglePage": { - "name": "Kết hợp thành một trang", - "subtitle": "Ghép tất cả các trang thành một cuộn liên tục." - }, - "viewMetadata": { - "name": "Xem Metadata", - "subtitle": "Kiểm tra các thuộc tính ẩn của PDF của bạn." - }, - "editMetadata": { - "name": "Chỉnh sửa Metadata", - "subtitle": "Thay đổi tác giả, tiêu đề và các thuộc tính khác." - }, - "pdfsToZip": { - "name": "PDF sang ZIP", - "subtitle": "Đóng gói nhiều tệp PDF thành kho lưu trữ ZIP." - }, - "comparePdfs": { - "name": "So sánh PDF", - "subtitle": "So sánh hai PDF cạnh nhau." - }, - "posterizePdf": { - "name": "Posterize PDF", - "subtitle": "Chia một trang lớn thành nhiều trang nhỏ hơn." - }, - "fixPageSize": { - "name": "Sửa kích thước trang", - "subtitle": "Chuẩn hóa tất cả các trang về cùng một kích thước." - }, - "linearizePdf": { - "name": "Tuyến tính hóa PDF", - "subtitle": "Tối ưu hóa PDF để xem web nhanh." - }, - "pageDimensions": { - "name": "Kích thước trang", - "subtitle": "Phân tích kích thước trang, hướng và đơn vị." - }, - "removeRestrictions": { - "name": "Xóa hạn chế", - "subtitle": "Xóa bảo vệ mật khẩu và hạn chế bảo mật liên quan đến tệp PDF được ký số." - }, - "repairPdf": { - "name": "Sửa chữa PDF", - "subtitle": "Khôi phục dữ liệu từ tệp PDF bị hỏng hoặc hư hỏng." - }, - "encryptPdf": { - "name": "Mã hóa PDF", - "subtitle": "Khóa PDF của bạn bằng cách thêm mật khẩu." - }, - "sanitizePdf": { - "name": "Làm sạch PDF", - "subtitle": "Xóa metadata, ghi chú, script và nhiều hơn nữa." - }, - "decryptPdf": { - "name": "Giải mã PDF", - "subtitle": "Mở khóa PDF bằng cách xóa bảo vệ mật khẩu." - }, - "flattenPdf": { - "name": "Làm phẳng PDF", - "subtitle": "Làm cho trường form và ghi chú không thể chỉnh sửa." - }, - "removeMetadata": { - "name": "Xóa Metadata", - "subtitle": "Loại bỏ dữ liệu ẩn khỏi PDF của bạn." - }, - "changePermissions": { - "name": "Thay đổi quyền", - "subtitle": "Đặt hoặc thay đổi quyền người dùng trên PDF." - }, - "odtToPdf": { - "name": "ODT sang PDF", - "subtitle": "Chuyển đổi tệp OpenDocument Text sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp ODT", - "convertButton": "Chuyển đổi sang PDF" - }, - "csvToPdf": { - "name": "CSV sang PDF", - "subtitle": "Chuyển đổi tệp bảng tính CSV sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp CSV", - "convertButton": "Chuyển đổi sang PDF" - }, - "rtfToPdf": { - "name": "RTF sang PDF", - "subtitle": "Chuyển đổi tài liệu Rich Text Format sang PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp RTF", - "convertButton": "Chuyển đổi sang PDF" - }, - "wordToPdf": { - "name": "Word sang PDF", - "subtitle": "Chuyển đổi tài liệu Word (DOCX, DOC, ODT, RTF) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp DOCX, DOC, ODT, RTF", - "convertButton": "Chuyển đổi sang PDF" - }, - "excelToPdf": { - "name": "Excel sang PDF", - "subtitle": "Chuyển đổi bảng tính Excel (XLSX, XLS, ODS, CSV) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp XLSX, XLS, ODS, CSV", - "convertButton": "Chuyển đổi sang PDF" - }, - "powerpointToPdf": { - "name": "PowerPoint sang PDF", - "subtitle": "Chuyển đổi bài thuyết trình PowerPoint (PPTX, PPT, ODP) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp PPTX, PPT, ODP", - "convertButton": "Chuyển đổi sang PDF" - }, - "markdownToPdf": { - "name": "Markdown sang PDF", - "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", - "btnSyncScroll": "Cuộn đồng bộ", - "btnSettings": "Cài đặt", - "btnExportPdf": "Xuất PDF", - "settingsTitle": "Cài đặt Markdown", - "settingsPreset": "Cài đặt sẵn", - "presetDefault": "Mặc định (kiểu GFM)", - "presetCommonmark": "CommonMark (nghiêm ngặt)", - "presetZero": "Tối thiểu (không có tính năng)", - "settingsOptions": "Tùy chọn Markdown", - "optAllowHtml": "Cho phép thẻ HTML", - "optBreaks": "Chuyển đổi xuống dòng thành
", - "optLinkify": "Tự động chuyển URL thành liên kết", - "optTypographer": "Trình sắp chữ (dấu ngoặc thông minh, v.v.)" - }, - "pdfBooklet": { - "name": "Sách nhỏ PDF", - "subtitle": "Sắp xếp lại các trang để in sách nhỏ hai mặt. Gấp và đóng ghim để tạo sách nhỏ.", - "howItWorks": "Cách hoạt động:", - "step1": "Tải lên tệp PDF.", - "step2": "Các trang sẽ được sắp xếp lại theo thứ tự sách nhỏ.", - "step3": "In hai mặt, lật cạnh ngắn, gấp và đóng ghim.", - "paperSize": "Kích thước giấy", - "orientation": "Hướng", - "portrait": "Dọc", - "landscape": "Ngang", - "pagesPerSheet": "Số trang mỗi tờ", - "createBooklet": "Tạo sách nhỏ", - "processing": "Đang xử lý...", - "pageCount": "Số trang sẽ được bổ sung lên bội số của 4 nếu cần." - }, - "xpsToPdf": { - "name": "XPS sang PDF", - "subtitle": "Chuyển đổi tài liệu XPS/OXPS sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp XPS, OXPS", - "convertButton": "Chuyển đổi sang PDF" - }, - "mobiToPdf": { - "name": "MOBI sang PDF", - "subtitle": "Chuyển đổi sách điện tử MOBI sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp MOBI", - "convertButton": "Chuyển đổi sang PDF" - }, - "epubToPdf": { - "name": "EPUB sang PDF", - "subtitle": "Chuyển đổi sách điện tử EPUB sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp EPUB", - "convertButton": "Chuyển đổi sang PDF" - }, - "fb2ToPdf": { - "name": "FB2 sang PDF", - "subtitle": "Chuyển đổi sách điện tử FictionBook (FB2) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp FB2", - "convertButton": "Chuyển đổi sang PDF" - }, - "cbzToPdf": { - "name": "CBZ sang PDF", - "subtitle": "Chuyển đổi kho lưu trữ truyện tranh (CBZ/CBR) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp CBZ, CBR", - "convertButton": "Chuyển đổi sang PDF" - }, - "wpdToPdf": { - "name": "WPD sang PDF", - "subtitle": "Chuyển đổi tài liệu WordPerfect (WPD) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp WPD", - "convertButton": "Chuyển đổi sang PDF" - }, - "wpsToPdf": { - "name": "WPS sang PDF", - "subtitle": "Chuyển đổi tài liệu WPS Office sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp WPS", - "convertButton": "Chuyển đổi sang PDF" - }, - "xmlToPdf": { - "name": "XML sang PDF", - "subtitle": "Chuyển đổi tài liệu XML sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp XML", - "convertButton": "Chuyển đổi sang PDF" - }, - "pagesToPdf": { - "name": "Pages sang PDF", - "subtitle": "Chuyển đổi tài liệu Apple Pages sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp Pages", - "convertButton": "Chuyển đổi sang PDF" - }, - "odgToPdf": { - "name": "ODG sang PDF", - "subtitle": "Chuyển đổi tệp OpenDocument Graphics (ODG) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp ODG", - "convertButton": "Chuyển đổi sang PDF" - }, - "odsToPdf": { - "name": "ODS sang PDF", - "subtitle": "Chuyển đổi tệp OpenDocument Spreadsheet (ODS) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp ODS", - "convertButton": "Chuyển đổi sang PDF" - }, - "odpToPdf": { - "name": "ODP sang PDF", - "subtitle": "Chuyển đổi tệp OpenDocument Presentation (ODP) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp ODP", - "convertButton": "Chuyển đổi sang PDF" - }, - "pubToPdf": { - "name": "PUB sang PDF", - "subtitle": "Chuyển đổi tệp Microsoft Publisher (PUB) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp PUB", - "convertButton": "Chuyển đổi sang PDF" - }, - "vsdToPdf": { - "name": "VSD sang PDF", - "subtitle": "Chuyển đổi tệp Microsoft Visio (VSD, VSDX) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp VSD, VSDX", - "convertButton": "Chuyển đổi sang PDF" - }, - "psdToPdf": { - "name": "PSD sang PDF", - "subtitle": "Chuyển đổi tệp Adobe Photoshop (PSD) sang định dạng PDF. Hỗ trợ nhiều tệp.", - "acceptedFormats": "Tệp PSD", - "convertButton": "Chuyển đổi sang PDF" - }, - "pdfToSvg": { - "name": "PDF sang SVG", - "subtitle": "Chuyển đổi mỗi trang PDF thành đồ họa vector có thể mở rộng (SVG) với chất lượng hoàn hảo ở mọi kích thước." - }, - "extractTables": { - "name": "Trích xuất bảng PDF", - "subtitle": "Trích xuất bảng từ tệp PDF và xuất dưới dạng CSV, JSON hoặc Markdown." - }, - "pdfToCsv": { - "name": "PDF sang CSV", - "subtitle": "Trích xuất bảng từ PDF và chuyển đổi sang định dạng CSV." - }, - "pdfToExcel": { - "name": "PDF sang Excel", - "subtitle": "Trích xuất bảng từ PDF và chuyển đổi sang định dạng Excel (XLSX)." - }, - "pdfToText": { - "name": "PDF sang Văn bản", - "subtitle": "Trích xuất văn bản từ tệp PDF và lưu dưới dạng tệp văn bản (.txt). Hỗ trợ nhiều tệp.", - "note": "Công cụ này CHỈ hoạt động với các tệp PDF được tạo kỹ thuật số. Đối với tài liệu quét hoặc PDF dựa trên hình ảnh, hãy sử dụng công cụ OCR PDF của chúng tôi.", - "convertButton": "Trích xuất văn bản" - }, - "digitalSignPdf": { - "name": "Chữ ký số PDF", - "pageTitle": "Chữ ký số PDF - Thêm chữ ký mật mã | BentoPDF", - "subtitle": "Thêm chữ ký số mật mã vào PDF của bạn bằng chứng chỉ X.509. Hỗ trợ định dạng PKCS#12 (.pfx, .p12) và PEM. Khóa riêng của bạn không bao giờ rời khỏi trình duyệt.", - "certificateSection": "Chứng chỉ", - "uploadCert": "Tải lên chứng chỉ (.pfx, .p12)", - "certPassword": "Mật khẩu chứng chỉ", - "certPasswordPlaceholder": "Nhập mật khẩu chứng chỉ", - "certInfo": "Thông tin chứng chỉ", - "certSubject": "Chủ thể", - "certIssuer": "Nhà phát hành", - "certValidity": "Hiệu lực", - "signatureDetails": "Chi tiết chữ ký (Tùy chọn)", - "reason": "Lý do", - "reasonPlaceholder": "ví dụ: Tôi phê duyệt tài liệu này", - "location": "Địa điểm", - "locationPlaceholder": "ví dụ: Hà Nội, Việt Nam", - "contactInfo": "Thông tin liên hệ", - "contactPlaceholder": "ví dụ: email@example.com", - "applySignature": "Áp dụng chữ ký số", - "successMessage": "Ký PDF thành công! Chữ ký có thể được xác minh trong bất kỳ trình đọc PDF nào." - }, - "validateSignaturePdf": { - "name": "Xác minh chữ ký PDF", - "pageTitle": "Xác minh chữ ký PDF - Xác thực chữ ký số | BentoPDF", - "subtitle": "Xác minh chữ ký số trong tệp PDF của bạn. Kiểm tra hiệu lực chứng chỉ, xem thông tin người ký và xác nhận tính toàn vẹn tài liệu. Tất cả xử lý diễn ra trong trình duyệt của bạn." - } -} \ No newline at end of file + "categories": { + "popularTools": "Công cụ phổ biến", + "editAnnotate": "Chỉnh sửa & Ghi chú", + "convertToPdf": "Chuyển đổi sang PDF", + "convertFromPdf": "Chuyển đổi từ PDF", + "organizeManage": "Sắp xếp & Quản lý", + "optimizeRepair": "Tối ưu hóa & Sửa chữa", + "securePdf": "Bảo mật PDF" + }, + "pdfMultiTool": { + "name": "Công cụ đa năng PDF", + "subtitle": "Gộp, Chia, Sắp xếp, Xóa, Xoay, Thêm trang trống, Trích xuất và Nhân đôi trong một giao diện thống nhất." + }, + "mergePdf": { + "name": "Gộp PDF", + "subtitle": "Kết hợp nhiều PDF thành một tệp. Giữ nguyên Bookmark." + }, + "splitPdf": { + "name": "Chia PDF", + "subtitle": "Trích xuất một phạm vi trang thành PDF mới." + }, + "compressPdf": { + "name": "Nén PDF", + "subtitle": "Giảm kích thước tệp PDF của bạn.", + "algorithmLabel": "Thuật toán nén", + "condense": "Condense (Khuyến nghị)", + "photon": "Photon (Dành cho PDF nhiều ảnh)", + "condenseInfo": "Condense sử dụng nén nâng cao: loại bỏ dữ liệu thừa, tối ưu hóa hình ảnh, gọn phông chữ. Phù hợp với hầu hết PDF.", + "photonInfo": "Photon chuyển đổi trang thành hình ảnh. Dùng cho PDF nhiều ảnh/quét.", + "photonWarning": "Cảnh báo: Văn bản sẽ không thể chọn được và liên kết sẽ không hoạt động.", + "levelLabel": "Mức độ nén", + "light": "Nhẹ (Giữ chất lượng)", + "balanced": "Cân bằng (Khuyến nghị)", + "aggressive": "Mạnh (Tệp nhỏ hơn)", + "extreme": "Cực đoan (Nén tối đa)", + "grayscale": "Chuyển sang thang xám", + "grayscaleHint": "Giảm kích thước tệp bằng cách loại bỏ thông tin màu", + "customSettings": "Cài đặt tùy chỉnh", + "customSettingsHint": "Tinh chỉnh các thông số nén:", + "outputQuality": "Chất lượng đầu ra", + "resizeImagesTo": "Thay đổi kích thước ảnh thành", + "onlyProcessAbove": "Chỉ xử lý khi trên", + "removeMetadata": "Xóa siêu dữ liệu", + "subsetFonts": "Gọn phông chữ (xóa ký tự không dùng)", + "removeThumbnails": "Xóa hình thu nhỏ nhúng", + "compressButton": "Nén PDF" + }, + "pdfEditor": { + "name": "Trình chỉnh sửa PDF", + "subtitle": "Ghi chú, tô sáng, chỉnh sửa, bình luận, thêm hình dạng/hình ảnh, tìm kiếm và xem PDF." + }, + "jpgToPdf": { + "name": "JPG sang PDF", + "subtitle": "Tạo PDF từ hình ảnh JPG, JPEG và JPEG2000 (JP2/JPX)." + }, + "signPdf": { + "name": "Ký PDF", + "subtitle": "Vẽ, gõ hoặc tải lên chữ ký của bạn." + }, + "cropPdf": { + "name": "Cắt PDF", + "subtitle": "Cắt lề của mọi trang trong PDF của bạn." + }, + "extractPages": { + "name": "Trích xuất trang", + "subtitle": "Lưu một lựa chọn trang dưới dạng tệp mới." + }, + "duplicateOrganize": { + "name": "Nhân đôi & Sắp xếp", + "subtitle": "Nhân đôi, sắp xếp lại và xóa trang." + }, + "deletePages": { + "name": "Xóa trang", + "subtitle": "Xóa các trang cụ thể khỏi tài liệu của bạn." + }, + "editBookmarks": { + "name": "Chỉnh sửa Bookmark", + "subtitle": "Thêm, chỉnh sửa, nhập, xóa và trích xuất bookmark PDF." + }, + "tableOfContents": { + "name": "Mục lục", + "subtitle": "Tạo trang mục lục từ bookmark PDF." + }, + "pageNumbers": { + "name": "Số trang", + "subtitle": "Chèn số trang vào tài liệu của bạn." + }, + "addWatermark": { + "name": "Thêm Watermark", + "subtitle": "Đóng dấu văn bản hoặc hình ảnh lên các trang PDF của bạn." + }, + "headerFooter": { + "name": "Đầu trang & Chân trang", + "subtitle": "Thêm văn bản vào đầu và cuối trang." + }, + "invertColors": { + "name": "Đảo ngược màu", + "subtitle": "Tạo phiên bản \"chế độ tối\" cho PDF của bạn." + }, + "backgroundColor": { + "name": "Màu nền", + "subtitle": "Thay đổi màu nền của PDF của bạn." + }, + "changeTextColor": { + "name": "Thay đổi màu văn bản", + "subtitle": "Thay đổi màu văn bản trong PDF của bạn." + }, + "addStamps": { + "name": "Thêm tem", + "subtitle": "Thêm tem hình ảnh vào PDF của bạn bằng thanh công cụ ghi chú.", + "usernameLabel": "Tên người dùng tem", + "usernamePlaceholder": "Nhập tên của bạn (cho tem)", + "usernameHint": "Tên này sẽ xuất hiện trên các tem bạn tạo." + }, + "removeAnnotations": { + "name": "Xóa ghi chú", + "subtitle": "Loại bỏ bình luận, tô sáng và liên kết." + }, + "pdfFormFiller": { + "name": "Điền form PDF", + "subtitle": "Điền form trực tiếp trong trình duyệt. Cũng hỗ trợ form XFA." + }, + "createPdfForm": { + "name": "Tạo form PDF", + "subtitle": "Tạo form PDF có thể điền với các trường văn bản kéo và thả." + }, + "removeBlankPages": { + "name": "Xóa trang trống", + "subtitle": "Tự động phát hiện và xóa trang trống." + }, + "imageToPdf": { + "name": "Hình ảnh sang PDF", + "subtitle": "Chuyển đổi JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP sang PDF." + }, + "pngToPdf": { + "name": "PNG sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh PNG." + }, + "webpToPdf": { + "name": "WebP sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh WebP." + }, + "svgToPdf": { + "name": "SVG sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh SVG." + }, + "bmpToPdf": { + "name": "BMP sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh BMP." + }, + "heicToPdf": { + "name": "HEIC sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh HEIC." + }, + "tiffToPdf": { + "name": "TIFF sang PDF", + "subtitle": "Tạo PDF từ một hoặc nhiều hình ảnh TIFF." + }, + "textToPdf": { + "name": "Văn bản sang PDF", + "subtitle": "Chuyển đổi tệp văn bản thuần túy thành PDF." + }, + "jsonToPdf": { + "name": "JSON sang PDF", + "subtitle": "Chuyển đổi tệp JSON sang định dạng PDF." + }, + "pdfToJpg": { + "name": "PDF sang JPG", + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh JPG." + }, + "pdfToPng": { + "name": "PDF sang PNG", + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh PNG." + }, + "pdfToWebp": { + "name": "PDF sang WebP", + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh WebP." + }, + "pdfToBmp": { + "name": "PDF sang BMP", + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh BMP." + }, + "pdfToTiff": { + "name": "PDF sang TIFF", + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh TIFF." + }, + "pdfToGreyscale": { + "name": "PDF sang thang xám", + "subtitle": "Chuyển đổi tất cả màu sắc sang đen trắng." + }, + "pdfToJson": { + "name": "PDF sang JSON", + "subtitle": "Chuyển đổi tệp PDF sang định dạng JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Làm cho PDF có thể tìm kiếm và sao chép được." + }, + "alternateMix": { + "name": "Xen kẽ & Trộn trang", + "subtitle": "Gộp PDF bằng cách xen kẽ trang từ mỗi PDF. Giữ nguyên Bookmark." + }, + "addAttachments": { + "name": "Thêm tệp đính kèm", + "subtitle": "Nhúng một hoặc nhiều tệp vào PDF của bạn." + }, + "extractAttachments": { + "name": "Trích xuất tệp đính kèm", + "subtitle": "Trích xuất tất cả tệp được nhúng từ PDF thành ZIP." + }, + "editAttachments": { + "name": "Chỉnh sửa tệp đính kèm", + "subtitle": "Xem hoặc xóa tệp đính kèm trong PDF của bạn." + }, + "dividePages": { + "name": "Chia trang", + "subtitle": "Chia trang theo chiều ngang hoặc chiều dọc." + }, + "addBlankPage": { + "name": "Thêm trang trống", + "subtitle": "Chèn trang trống ở bất kỳ đâu trong PDF của bạn." + }, + "reversePages": { + "name": "Đảo ngược trang", + "subtitle": "Lật ngược thứ tự tất cả các trang trong tài liệu của bạn." + }, + "rotatePdf": { + "name": "Xoay PDF", + "subtitle": "Xoay trang theo bội số 90 độ." + }, + "rotateCustom": { + "name": "Xoay theo độ tùy chỉnh", + "subtitle": "Xoay trang theo bất kỳ góc độ tùy chỉnh nào." + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "Sắp xếp nhiều trang lên một tờ." + }, + "combineToSinglePage": { + "name": "Kết hợp thành một trang", + "subtitle": "Ghép tất cả các trang thành một cuộn liên tục." + }, + "viewMetadata": { + "name": "Xem Metadata", + "subtitle": "Kiểm tra các thuộc tính ẩn của PDF của bạn." + }, + "editMetadata": { + "name": "Chỉnh sửa Metadata", + "subtitle": "Thay đổi tác giả, tiêu đề và các thuộc tính khác." + }, + "pdfsToZip": { + "name": "PDF sang ZIP", + "subtitle": "Đóng gói nhiều tệp PDF thành kho lưu trữ ZIP." + }, + "comparePdfs": { + "name": "So sánh PDF", + "subtitle": "So sánh hai PDF cạnh nhau." + }, + "posterizePdf": { + "name": "Posterize PDF", + "subtitle": "Chia một trang lớn thành nhiều trang nhỏ hơn." + }, + "fixPageSize": { + "name": "Sửa kích thước trang", + "subtitle": "Chuẩn hóa tất cả các trang về cùng một kích thước." + }, + "linearizePdf": { + "name": "Tuyến tính hóa PDF", + "subtitle": "Tối ưu hóa PDF để xem web nhanh." + }, + "pageDimensions": { + "name": "Kích thước trang", + "subtitle": "Phân tích kích thước trang, hướng và đơn vị." + }, + "removeRestrictions": { + "name": "Xóa hạn chế", + "subtitle": "Xóa bảo vệ mật khẩu và hạn chế bảo mật liên quan đến tệp PDF được ký số." + }, + "repairPdf": { + "name": "Sửa chữa PDF", + "subtitle": "Khôi phục dữ liệu từ tệp PDF bị hỏng hoặc hư hỏng." + }, + "encryptPdf": { + "name": "Mã hóa PDF", + "subtitle": "Khóa PDF của bạn bằng cách thêm mật khẩu." + }, + "sanitizePdf": { + "name": "Làm sạch PDF", + "subtitle": "Xóa metadata, ghi chú, script và nhiều hơn nữa." + }, + "decryptPdf": { + "name": "Giải mã PDF", + "subtitle": "Mở khóa PDF bằng cách xóa bảo vệ mật khẩu." + }, + "flattenPdf": { + "name": "Làm phẳng PDF", + "subtitle": "Làm cho trường form và ghi chú không thể chỉnh sửa." + }, + "removeMetadata": { + "name": "Xóa Metadata", + "subtitle": "Loại bỏ dữ liệu ẩn khỏi PDF của bạn." + }, + "changePermissions": { + "name": "Thay đổi quyền", + "subtitle": "Đặt hoặc thay đổi quyền người dùng trên PDF." + }, + "odtToPdf": { + "name": "ODT sang PDF", + "subtitle": "Chuyển đổi tệp OpenDocument Text sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp ODT", + "convertButton": "Chuyển đổi sang PDF" + }, + "csvToPdf": { + "name": "CSV sang PDF", + "subtitle": "Chuyển đổi tệp bảng tính CSV sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp CSV", + "convertButton": "Chuyển đổi sang PDF" + }, + "rtfToPdf": { + "name": "RTF sang PDF", + "subtitle": "Chuyển đổi tài liệu Rich Text Format sang PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp RTF", + "convertButton": "Chuyển đổi sang PDF" + }, + "wordToPdf": { + "name": "Word sang PDF", + "subtitle": "Chuyển đổi tài liệu Word (DOCX, DOC, ODT, RTF) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp DOCX, DOC, ODT, RTF", + "convertButton": "Chuyển đổi sang PDF" + }, + "excelToPdf": { + "name": "Excel sang PDF", + "subtitle": "Chuyển đổi bảng tính Excel (XLSX, XLS, ODS, CSV) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp XLSX, XLS, ODS, CSV", + "convertButton": "Chuyển đổi sang PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint sang PDF", + "subtitle": "Chuyển đổi bài thuyết trình PowerPoint (PPTX, PPT, ODP) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp PPTX, PPT, ODP", + "convertButton": "Chuyển đổi sang PDF" + }, + "markdownToPdf": { + "name": "Markdown sang PDF", + "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", + "btnSyncScroll": "Cuộn đồng bộ", + "btnSettings": "Cài đặt", + "btnExportPdf": "Xuất PDF", + "settingsTitle": "Cài đặt Markdown", + "settingsPreset": "Cài đặt sẵn", + "presetDefault": "Mặc định (kiểu GFM)", + "presetCommonmark": "CommonMark (nghiêm ngặt)", + "presetZero": "Tối thiểu (không có tính năng)", + "settingsOptions": "Tùy chọn Markdown", + "optAllowHtml": "Cho phép thẻ HTML", + "optBreaks": "Chuyển đổi xuống dòng thành
", + "optLinkify": "Tự động chuyển URL thành liên kết", + "optTypographer": "Trình sắp chữ (dấu ngoặc thông minh, v.v.)" + }, + "pdfBooklet": { + "name": "Sách nhỏ PDF", + "subtitle": "Sắp xếp lại các trang để in sách nhỏ hai mặt. Gấp và đóng ghim để tạo sách nhỏ.", + "howItWorks": "Cách hoạt động:", + "step1": "Tải lên tệp PDF.", + "step2": "Các trang sẽ được sắp xếp lại theo thứ tự sách nhỏ.", + "step3": "In hai mặt, lật cạnh ngắn, gấp và đóng ghim.", + "paperSize": "Kích thước giấy", + "orientation": "Hướng", + "portrait": "Dọc", + "landscape": "Ngang", + "pagesPerSheet": "Số trang mỗi tờ", + "createBooklet": "Tạo sách nhỏ", + "processing": "Đang xử lý...", + "pageCount": "Số trang sẽ được bổ sung lên bội số của 4 nếu cần." + }, + "xpsToPdf": { + "name": "XPS sang PDF", + "subtitle": "Chuyển đổi tài liệu XPS/OXPS sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp XPS, OXPS", + "convertButton": "Chuyển đổi sang PDF" + }, + "mobiToPdf": { + "name": "MOBI sang PDF", + "subtitle": "Chuyển đổi sách điện tử MOBI sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp MOBI", + "convertButton": "Chuyển đổi sang PDF" + }, + "epubToPdf": { + "name": "EPUB sang PDF", + "subtitle": "Chuyển đổi sách điện tử EPUB sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp EPUB", + "convertButton": "Chuyển đổi sang PDF" + }, + "fb2ToPdf": { + "name": "FB2 sang PDF", + "subtitle": "Chuyển đổi sách điện tử FictionBook (FB2) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp FB2", + "convertButton": "Chuyển đổi sang PDF" + }, + "cbzToPdf": { + "name": "CBZ sang PDF", + "subtitle": "Chuyển đổi kho lưu trữ truyện tranh (CBZ/CBR) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp CBZ, CBR", + "convertButton": "Chuyển đổi sang PDF" + }, + "wpdToPdf": { + "name": "WPD sang PDF", + "subtitle": "Chuyển đổi tài liệu WordPerfect (WPD) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp WPD", + "convertButton": "Chuyển đổi sang PDF" + }, + "wpsToPdf": { + "name": "WPS sang PDF", + "subtitle": "Chuyển đổi tài liệu WPS Office sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp WPS", + "convertButton": "Chuyển đổi sang PDF" + }, + "xmlToPdf": { + "name": "XML sang PDF", + "subtitle": "Chuyển đổi tài liệu XML sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp XML", + "convertButton": "Chuyển đổi sang PDF" + }, + "pagesToPdf": { + "name": "Pages sang PDF", + "subtitle": "Chuyển đổi tài liệu Apple Pages sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp Pages", + "convertButton": "Chuyển đổi sang PDF" + }, + "odgToPdf": { + "name": "ODG sang PDF", + "subtitle": "Chuyển đổi tệp OpenDocument Graphics (ODG) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp ODG", + "convertButton": "Chuyển đổi sang PDF" + }, + "odsToPdf": { + "name": "ODS sang PDF", + "subtitle": "Chuyển đổi tệp OpenDocument Spreadsheet (ODS) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp ODS", + "convertButton": "Chuyển đổi sang PDF" + }, + "odpToPdf": { + "name": "ODP sang PDF", + "subtitle": "Chuyển đổi tệp OpenDocument Presentation (ODP) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp ODP", + "convertButton": "Chuyển đổi sang PDF" + }, + "pubToPdf": { + "name": "PUB sang PDF", + "subtitle": "Chuyển đổi tệp Microsoft Publisher (PUB) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp PUB", + "convertButton": "Chuyển đổi sang PDF" + }, + "vsdToPdf": { + "name": "VSD sang PDF", + "subtitle": "Chuyển đổi tệp Microsoft Visio (VSD, VSDX) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp VSD, VSDX", + "convertButton": "Chuyển đổi sang PDF" + }, + "psdToPdf": { + "name": "PSD sang PDF", + "subtitle": "Chuyển đổi tệp Adobe Photoshop (PSD) sang định dạng PDF. Hỗ trợ nhiều tệp.", + "acceptedFormats": "Tệp PSD", + "convertButton": "Chuyển đổi sang PDF" + }, + "pdfToSvg": { + "name": "PDF sang SVG", + "subtitle": "Chuyển đổi mỗi trang PDF thành đồ họa vector có thể mở rộng (SVG) với chất lượng hoàn hảo ở mọi kích thước." + }, + "extractTables": { + "name": "Trích xuất bảng PDF", + "subtitle": "Trích xuất bảng từ tệp PDF và xuất dưới dạng CSV, JSON hoặc Markdown." + }, + "pdfToCsv": { + "name": "PDF sang CSV", + "subtitle": "Trích xuất bảng từ PDF và chuyển đổi sang định dạng CSV." + }, + "pdfToExcel": { + "name": "PDF sang Excel", + "subtitle": "Trích xuất bảng từ PDF và chuyển đổi sang định dạng Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF sang Văn bản", + "subtitle": "Trích xuất văn bản từ tệp PDF và lưu dưới dạng tệp văn bản (.txt). Hỗ trợ nhiều tệp.", + "note": "Công cụ này CHỈ hoạt động với các tệp PDF được tạo kỹ thuật số. Đối với tài liệu quét hoặc PDF dựa trên hình ảnh, hãy sử dụng công cụ OCR PDF của chúng tôi.", + "convertButton": "Trích xuất văn bản" + }, + "digitalSignPdf": { + "name": "Chữ ký số PDF", + "pageTitle": "Chữ ký số PDF - Thêm chữ ký mật mã | BentoPDF", + "subtitle": "Thêm chữ ký số mật mã vào PDF của bạn bằng chứng chỉ X.509. Hỗ trợ định dạng PKCS#12 (.pfx, .p12) và PEM. Khóa riêng của bạn không bao giờ rời khỏi trình duyệt.", + "certificateSection": "Chứng chỉ", + "uploadCert": "Tải lên chứng chỉ (.pfx, .p12)", + "certPassword": "Mật khẩu chứng chỉ", + "certPasswordPlaceholder": "Nhập mật khẩu chứng chỉ", + "certInfo": "Thông tin chứng chỉ", + "certSubject": "Chủ thể", + "certIssuer": "Nhà phát hành", + "certValidity": "Hiệu lực", + "signatureDetails": "Chi tiết chữ ký (Tùy chọn)", + "reason": "Lý do", + "reasonPlaceholder": "ví dụ: Tôi phê duyệt tài liệu này", + "location": "Địa điểm", + "locationPlaceholder": "ví dụ: Hà Nội, Việt Nam", + "contactInfo": "Thông tin liên hệ", + "contactPlaceholder": "ví dụ: email@example.com", + "applySignature": "Áp dụng chữ ký số", + "successMessage": "Ký PDF thành công! Chữ ký có thể được xác minh trong bất kỳ trình đọc PDF nào." + }, + "validateSignaturePdf": { + "name": "Xác minh chữ ký PDF", + "pageTitle": "Xác minh chữ ký PDF - Xác thực chữ ký số | BentoPDF", + "subtitle": "Xác minh chữ ký số trong tệp PDF của bạn. Kiểm tra hiệu lực chứng chỉ, xem thông tin người ký và xác nhận tính toàn vẹn tài liệu. Tất cả xử lý diễn ra trong trình duyệt của bạn." + }, + "emailToPdf": { + "name": "Email sang PDF", + "subtitle": "Chuyển đổi tệp email (EML, MSG) sang định dạng PDF. Hỗ trợ xuất Outlook và định dạng email tiêu chuẩn.", + "acceptedFormats": "Tệp EML, MSG", + "convertButton": "Chuyển đổi sang PDF" + } +} diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index 832c42287..fca82d65d 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -1,516 +1,522 @@ { - "categories": { - "popularTools": "热门工具", - "editAnnotate": "编辑与注释", - "convertToPdf": "转换为 PDF", - "convertFromPdf": "从 PDF 转换", - "organizeManage": "组织与管理", - "optimizeRepair": "优化与修复", - "securePdf": "安全 PDF" - }, - "pdfMultiTool": { - "name": "PDF 多功能工具", - "subtitle": "在一个统一的界面中合并、分割、组织、删除、旋转、添加空白页、提取和复制。" - }, - "mergePdf": { - "name": "合并 PDF", - "subtitle": "将多个 PDF 合并为一个文件。保留书签。" - }, - "splitPdf": { - "name": "分割 PDF", - "subtitle": "将指定范围的页面提取到新 PDF 中。" - }, - "compressPdf": { - "name": "压缩 PDF", - "subtitle": "减小您的 PDF 文件大小。", - "algorithmLabel": "压缩算法", - "condense": "Condense(推荐)", - "photon": "Photon(适用于图片较多的 PDF)", - "condenseInfo": "Condense 使用高级压缩:移除冗余数据、优化图片、精简字体。适用于大多数 PDF。", - "photonInfo": "Photon 将页面转换为图片。适用于图片较多/扫描的 PDF。", - "photonWarning": "警告:文本将无法选择,链接将失效。", - "levelLabel": "压缩级别", - "light": "轻度(保持质量)", - "balanced": "平衡(推荐)", - "aggressive": "积极(更小文件)", - "extreme": "极限(最大压缩)", - "grayscale": "转换为灰度", - "grayscaleHint": "通过移除颜色信息来减小文件大小", - "customSettings": "自定义设置", - "customSettingsHint": "微调压缩参数:", - "outputQuality": "输出质量", - "resizeImagesTo": "调整图片至", - "onlyProcessAbove": "仅处理高于", - "removeMetadata": "移除元数据", - "subsetFonts": "精简字体(移除未使用的字符)", - "removeThumbnails": "移除嵌入的缩略图", - "compressButton": "压缩 PDF" - }, - "pdfEditor": { - "name": "PDF 编辑器", - "subtitle": "注释、高亮、涂黑、评论、添加形状/图片、搜索和查看 PDF。" - }, - "jpgToPdf": { - "name": "JPG 转 PDF", - "subtitle": "从 JPG、JPEG 和 JPEG2000 (JP2/JPX) 图片创建 PDF。" - }, - "signPdf": { - "name": "签署 PDF", - "subtitle": "绘制、键入或上传您的签名。" - }, - "cropPdf": { - "name": "裁剪 PDF", - "subtitle": "修剪 PDF 中每一页的边距。" - }, - "extractPages": { - "name": "提取页面", - "subtitle": "将选定的页面保存为新文件。" - }, - "duplicateOrganize": { - "name": "复制与组织", - "subtitle": "复制、重新排序和删除页面。" - }, - "deletePages": { - "name": "删除页面", - "subtitle": "自您的文档中移除特定页面。" - }, - "editBookmarks": { - "name": "编辑书签", - "subtitle": "添加、编辑、导入、删除和提取 PDF 书签。" - }, - "tableOfContents": { - "name": "目录", - "subtitle": "根据 PDF 书签生成目录页。" - }, - "pageNumbers": { - "name": "页码", - "subtitle": "将页码插入到您的文档中。" - }, - "addWatermark": { - "name": "添加水印", - "subtitle": "在您的 PDF 页面上添加文字或图片水印。" - }, - "headerFooter": { - "name": "页眉和页脚", - "subtitle": "在页面顶部和底部添加文字。" - }, - "invertColors": { - "name": "反转颜色", - "subtitle": "创建您的 PDF 的“暗黑模式”版本。" - }, - "backgroundColor": { - "name": "背景颜色", - "subtitle": "更改您的 PDF 的背景颜色。" - }, - "changeTextColor": { - "name": "更改文本颜色", - "subtitle": "更改您 PDF 中文本的颜色。" - }, - "addStamps": { - "name": "添加印章", - "subtitle": "使用注释工具栏向您的 PDF 添加图片印章。" - }, - "removeAnnotations": { - "name": "移除注释", - "subtitle": "移除评论、高亮和链接。" - }, - "pdfFormFiller": { - "name": "PDF 表单填写器", - "subtitle": "直接在浏览器中填写表单。也支持 XFA 表单。" - }, - "createPdfForm": { - "name": "创建 PDF 表单", - "subtitle": "使用拖放文本字段创建可填写的 PDF 表单。" - }, - "removeBlankPages": { - "name": "移除空白页", - "subtitle": "自动检测并删除空白页。" - }, - "imageToPdf": { - "name": "图片转 PDF", - "subtitle": "将 JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP 转换为 PDF。" - }, - "pngToPdf": { - "name": "PNG 转 PDF", - "subtitle": "从一张或多张 PNG 图片创建 PDF。" - }, - "webpToPdf": { - "name": "WebP 转 PDF", - "subtitle": "从一张或多张 WebP 图片创建 PDF。" - }, - "svgToPdf": { - "name": "SVG 转 PDF", - "subtitle": "从一张或多张 SVG 图片创建 PDF。" - }, - "bmpToPdf": { - "name": "BMP 转 PDF", - "subtitle": "从一张或多张 BMP 图片创建 PDF。" - }, - "heicToPdf": { - "name": "HEIC 转 PDF", - "subtitle": "从一张或多张 HEIC 图片创建 PDF。" - }, - "tiffToPdf": { - "name": "TIFF 转 PDF", - "subtitle": "从一张或多张 TIFF 图片创建 PDF。" - }, - "textToPdf": { - "name": "文本转 PDF", - "subtitle": "将纯文本文件转换为 PDF。" - }, - "jsonToPdf": { - "name": "JSON 转 PDF", - "subtitle": "将 JSON 文件转换为 PDF 格式。" - }, - "pdfToJpg": { - "name": "PDF 转 JPG", - "subtitle": "将每一页 PDF 转换为 JPG 图片。" - }, - "pdfToPng": { - "name": "PDF 转 PNG", - "subtitle": "将每一页 PDF 转换为 PNG 图片。" - }, - "pdfToWebp": { - "name": "PDF 转 WebP", - "subtitle": "将每一页 PDF 转换为 WebP 图片。" - }, - "pdfToBmp": { - "name": "PDF 转 BMP", - "subtitle": "将每一页 PDF 转换为 BMP 图片。" - }, - "pdfToTiff": { - "name": "PDF 转 TIFF", - "subtitle": "将每一页 PDF 转换为 TIFF 图片。" - }, - "pdfToGreyscale": { - "name": "PDF 转 灰度", - "subtitle": "将所有颜色转换为黑白。" - }, - "pdfToJson": { - "name": "PDF 转 JSON", - "subtitle": "将 PDF 文件转换为 JSON 格式。" - }, - "ocrPdf": { - "name": "OCR PDF", - "subtitle": "使 PDF 可搜索和可复制。" - }, - "alternateMix": { - "name": "交替混合页面", - "subtitle": "通过交替每个 PDF 的页面来合并 PDF。保留书签。" - }, - "addAttachments": { - "name": "添加附件", - "subtitle": "将一个或多个文件嵌入到您的 PDF 中。" - }, - "extractAttachments": { - "name": "提取附件", - "subtitle": "从 PDF 中提取所有嵌入的文件为 ZIP。" - }, - "editAttachments": { - "name": "编辑附件", - "subtitle": "查看或移除 PDF 中的附件。" - }, - "dividePages": { - "name": "分割页面", - "subtitle": "水平或垂直分割页面。" - }, - "addBlankPage": { - "name": "添加空白页", - "subtitle": "在 PDF 的任意位置插入空白页。" - }, - "reversePages": { - "name": "反转页面", - "subtitle": "反转文档中所有页面的顺序。" - }, - "rotatePdf": { - "name": "旋转 PDF", - "subtitle": "以 90 度增量旋转页面。" - }, - "rotateCustom": { - "name": "按自定义角度旋转", - "subtitle": "按任意自定义角度旋转页面。" - }, - "nUpPdf": { - "name": "N-Up PDF", - "subtitle": "将多页排列在单张纸上。" - }, - "combineToSinglePage": { - "name": "合并为单页", - "subtitle": "将所有页面拼接成一个连续的滚动页面。" - }, - "viewMetadata": { - "name": "查看元数据", - "subtitle": "检查 PDF 的隐藏属性。" - }, - "editMetadata": { - "name": "编辑元数据", - "subtitle": "更改作者、标题和其他属性。" - }, - "pdfsToZip": { - "name": "PDF 转 ZIP", - "subtitle": "将多个 PDF 文件打包成一个 ZIP 归档。" - }, - "comparePdfs": { - "name": "比较 PDF", - "subtitle": "并排比较两个 PDF。" - }, - "posterizePdf": { - "name": "海报化 PDF", - "subtitle": "将大页面分割成多个小页面。" - }, - "fixPageSize": { - "name": "修复页面尺寸", - "subtitle": "将所有页面标准化为统一尺寸。" - }, - "linearizePdf": { - "name": "线性化 PDF", - "subtitle": "优化 PDF 以便快速网络查看。" - }, - "pageDimensions": { - "name": "页面尺寸", - "subtitle": "分析页面大小、方向和单位。" - }, - "removeRestrictions": { - "name": "移除限制", - "subtitle": "移除与数字签名 PDF 文件相关的密码保护和安全限制。" - }, - "repairPdf": { - "name": "修复 PDF", - "subtitle": "从损坏的 PDF 文件中恢复数据。" - }, - "encryptPdf": { - "name": "加密 PDF", - "subtitle": "通过添加密码锁定您的 PDF。" - }, - "sanitizePdf": { - "name": "清理 PDF", - "subtitle": "移除元数据、注释、脚本等。" - }, - "decryptPdf": { - "name": "解密 PDF", - "subtitle": "通过移除密码保护解锁 PDF。" - }, - "flattenPdf": { - "name": "扁平化 PDF", - "subtitle": "使表单字段和注释不可编辑。" - }, - "removeMetadata": { - "name": "移除元数据", - "subtitle": "从 PDF 中剥离隐藏数据。" - }, - "changePermissions": { - "name": "更改权限", - "subtitle": "设置或更改 PDF 上的用户权限。" - }, - "odtToPdf": { - "name": "ODT 转 PDF", - "subtitle": "将 OpenDocument 文本文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "ODT 文件", - "convertButton": "转换为 PDF" - }, - "csvToPdf": { - "name": "CSV 转 PDF", - "subtitle": "将 CSV 电子表格文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "CSV 件", - "convertButton": "转换为 PDF" - }, - "rtfToPdf": { - "name": "RTF 转 PDF", - "subtitle": "将富文本格式文档转换为 PDF。支持多个文件。", - "acceptedFormats": "RTF 文件", - "convertButton": "转换为 PDF" - }, - "wordToPdf": { - "name": "Word 转 PDF", - "subtitle": "将 Word 文档 (DOCX, DOC, ODT, RTF) 转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "DOCX, DOC, ODT, RTF 文件", - "convertButton": "转换为 PDF" - }, - "excelToPdf": { - "name": "Excel 转 PDF", - "subtitle": "将 Excel 电子表格 (XLSX, XLS, ODS, CSV) 转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "XLSX, XLS, ODS, CSV 文件", - "convertButton": "转换为 PDF" - }, - "powerpointToPdf": { - "name": "PowerPoint 转 PDF", - "subtitle": "将 PowerPoint 演示文稿 (PPTX, PPT, ODP) 转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "PPTX, PPT, ODP 文件", - "convertButton": "转换为 PDF" - }, - "markdownToPdf": { - "name": "Markdown 转 PDF", - "subtitle": "编写或粘贴 Markdown 并将其导出为精美格式的 PDF。", - "paneMarkdown": "Markdown", - "panePreview": "预览", - "btnUpload": "上传", - "btnSyncScroll": "同步滚动", - "btnSettings": "设置", - "btnExportPdf": "导出 PDF", - "settingsTitle": "Markdown 设置", - "settingsPreset": "预设", - "presetDefault": "默认 (GFM 风格)", - "presetCommonmark": "CommonMark (严格)", - "presetZero": "最小 (无功能)", - "settingsOptions": "Markdown 选项", - "optAllowHtml": "允许 HTML 标签", - "optBreaks": "将换行转换为
", - "optLinkify": "自动将 URL 转换为链接", - "optTypographer": "排版器 (智能引号等)" - }, - "pdfBooklet": { - "name": "PDF 小册子", - "subtitle": "重新排列页面用于双面小册子打印。折叠并装订以创建小册子。", - "howItWorks": "工作原理:", - "step1": "上传 PDF 文件。", - "step2": "页面将按小册子顺序重新排列。", - "step3": "双面打印,短边翻转,折叠并装订。", - "paperSize": "纸张大小", - "orientation": "方向", - "portrait": "纵向", - "landscape": "横向", - "pagesPerSheet": "每张页数", - "createBooklet": "创建小册子", - "processing": "处理中...", - "pageCount": "如需要,页数将补齐为 4 的倍数。" - }, - "xpsToPdf": { - "name": "XPS 转 PDF", - "subtitle": "将 XPS/OXPS 文档转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "XPS, OXPS 文件", - "convertButton": "转换为 PDF" - }, - "mobiToPdf": { - "name": "MOBI 转 PDF", - "subtitle": "将 MOBI 电子书转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "MOBI 文件", - "convertButton": "转换为 PDF" - }, - "epubToPdf": { - "name": "EPUB 转 PDF", - "subtitle": "将 EPUB 电子书转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "EPUB 文件", - "convertButton": "转换为 PDF" - }, - "fb2ToPdf": { - "name": "FB2 转 PDF", - "subtitle": "将 FictionBook (FB2) 电子书转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "FB2 文件", - "convertButton": "转换为 PDF" - }, - "cbzToPdf": { - "name": "CBZ 转 PDF", - "subtitle": "将漫画档案 (CBZ/CBR) 转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "CBZ, CBR 文件", - "convertButton": "转换为 PDF" - }, - "wpdToPdf": { - "name": "WPD 转 PDF", - "subtitle": "将 WordPerfect 文档 (WPD) 转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "WPD 文件", - "convertButton": "转换为 PDF" - }, - "wpsToPdf": { - "name": "WPS 转 PDF", - "subtitle": "将 WPS Office 文档转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "WPS 文件", - "convertButton": "转换为 PDF" - }, - "xmlToPdf": { - "name": "XML 转 PDF", - "subtitle": "将 XML 文档转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "XML 文件", - "convertButton": "转换为 PDF" - }, - "pagesToPdf": { - "name": "Pages 转 PDF", - "subtitle": "将 Apple Pages 文档转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "Pages 文件", - "convertButton": "转换为 PDF" - }, - "odgToPdf": { - "name": "ODG 转 PDF", - "subtitle": "将 OpenDocument Graphics (ODG) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "ODG 文件", - "convertButton": "转换为 PDF" - }, - "odsToPdf": { - "name": "ODS 转 PDF", - "subtitle": "将 OpenDocument Spreadsheet (ODS) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "ODS 文件", - "convertButton": "转换为 PDF" - }, - "odpToPdf": { - "name": "ODP 转 PDF", - "subtitle": "将 OpenDocument Presentation (ODP) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "ODP 文件", - "convertButton": "转换为 PDF" - }, - "pubToPdf": { - "name": "PUB 转 PDF", - "subtitle": "将 Microsoft Publisher (PUB) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "PUB 文件", - "convertButton": "转换为 PDF" - }, - "vsdToPdf": { - "name": "VSD 转 PDF", - "subtitle": "将 Microsoft Visio (VSD, VSDX) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "VSD, VSDX 文件", - "convertButton": "转换为 PDF" - }, - "psdToPdf": { - "name": "PSD 转 PDF", - "subtitle": "将 Adobe Photoshop (PSD) 文件转换为 PDF 格式。支持多个文件。", - "acceptedFormats": "PSD 文件", - "convertButton": "转换为 PDF" - }, - "pdfToSvg": { - "name": "PDF 转 SVG", - "subtitle": "将 PDF 文件的每一页转换为可缩放矢量图形 (SVG),在任何尺寸下都能保持完美质量。" - }, - "extractTables": { - "name": "提取 PDF 表格", - "subtitle": "从 PDF 文件中提取表格,并导出为 CSV、JSON 或 Markdown 格式。" - }, - "pdfToCsv": { - "name": "PDF 转 CSV", - "subtitle": "从 PDF 中提取表格并转换为 CSV 格式。" - }, - "pdfToExcel": { - "name": "PDF 转 Excel", - "subtitle": "从 PDF 中提取表格并转换为 Excel (XLSX) 格式。" - }, - "pdfToText": { - "name": "PDF 转 文本", - "subtitle": "从 PDF 文件中提取文本并保存为纯文本文件 (.txt)。支持多个文件。", - "note": "此工具仅适用于数字创建的 PDF。对于扫描文档或基于图像的 PDF,请使用我们的 OCR PDF 工具。", - "convertButton": "提取文本" - }, - "digitalSignPdf": { - "name": "PDF 数字签名", - "pageTitle": "PDF 数字签名 - 添加加密签名 | BentoPDF", - "subtitle": "使用 X.509 证书为您的 PDF 添加加密数字签名。支持 PKCS#12 (.pfx, .p12) 和 PEM 格式。您的私钥永远不会离开您的浏览器。", - "certificateSection": "证书", - "uploadCert": "上传证书 (.pfx, .p12)", - "certPassword": "证书密码", - "certPasswordPlaceholder": "输入证书密码", - "certInfo": "证书信息", - "certSubject": "主体", - "certIssuer": "颁发者", - "certValidity": "有效期", - "signatureDetails": "签名详情(可选)", - "reason": "原因", - "reasonPlaceholder": "例如:我批准此文档", - "location": "位置", - "locationPlaceholder": "例如:北京,中国", - "contactInfo": "联系信息", - "contactPlaceholder": "例如:email@example.com", - "applySignature": "应用数字签名", - "successMessage": "PDF 签名成功!签名可在任何 PDF 阅读器中验证。" - }, - "validateSignaturePdf": { - "name": "验证 PDF 签名", - "pageTitle": "验证 PDF 签名 - 验证数字签名 | BentoPDF", - "subtitle": "验证您的 PDF 文件中的数字签名。检查证书有效性、查看签名者详情并确认文档完整性。所有处理都在您的浏览器中进行。" - } -} \ No newline at end of file + "categories": { + "popularTools": "热门工具", + "editAnnotate": "编辑与注释", + "convertToPdf": "转换为 PDF", + "convertFromPdf": "从 PDF 转换", + "organizeManage": "组织与管理", + "optimizeRepair": "优化与修复", + "securePdf": "安全 PDF" + }, + "pdfMultiTool": { + "name": "PDF 多功能工具", + "subtitle": "在一个统一的界面中合并、分割、组织、删除、旋转、添加空白页、提取和复制。" + }, + "mergePdf": { + "name": "合并 PDF", + "subtitle": "将多个 PDF 合并为一个文件。保留书签。" + }, + "splitPdf": { + "name": "分割 PDF", + "subtitle": "将指定范围的页面提取到新 PDF 中。" + }, + "compressPdf": { + "name": "压缩 PDF", + "subtitle": "减小您的 PDF 文件大小。", + "algorithmLabel": "压缩算法", + "condense": "Condense(推荐)", + "photon": "Photon(适用于图片较多的 PDF)", + "condenseInfo": "Condense 使用高级压缩:移除冗余数据、优化图片、精简字体。适用于大多数 PDF。", + "photonInfo": "Photon 将页面转换为图片。适用于图片较多/扫描的 PDF。", + "photonWarning": "警告:文本将无法选择,链接将失效。", + "levelLabel": "压缩级别", + "light": "轻度(保持质量)", + "balanced": "平衡(推荐)", + "aggressive": "积极(更小文件)", + "extreme": "极限(最大压缩)", + "grayscale": "转换为灰度", + "grayscaleHint": "通过移除颜色信息来减小文件大小", + "customSettings": "自定义设置", + "customSettingsHint": "微调压缩参数:", + "outputQuality": "输出质量", + "resizeImagesTo": "调整图片至", + "onlyProcessAbove": "仅处理高于", + "removeMetadata": "移除元数据", + "subsetFonts": "精简字体(移除未使用的字符)", + "removeThumbnails": "移除嵌入的缩略图", + "compressButton": "压缩 PDF" + }, + "pdfEditor": { + "name": "PDF 编辑器", + "subtitle": "注释、高亮、涂黑、评论、添加形状/图片、搜索和查看 PDF。" + }, + "jpgToPdf": { + "name": "JPG 转 PDF", + "subtitle": "从 JPG、JPEG 和 JPEG2000 (JP2/JPX) 图片创建 PDF。" + }, + "signPdf": { + "name": "签署 PDF", + "subtitle": "绘制、键入或上传您的签名。" + }, + "cropPdf": { + "name": "裁剪 PDF", + "subtitle": "修剪 PDF 中每一页的边距。" + }, + "extractPages": { + "name": "提取页面", + "subtitle": "将选定的页面保存为新文件。" + }, + "duplicateOrganize": { + "name": "复制与组织", + "subtitle": "复制、重新排序和删除页面。" + }, + "deletePages": { + "name": "删除页面", + "subtitle": "自您的文档中移除特定页面。" + }, + "editBookmarks": { + "name": "编辑书签", + "subtitle": "添加、编辑、导入、删除和提取 PDF 书签。" + }, + "tableOfContents": { + "name": "目录", + "subtitle": "根据 PDF 书签生成目录页。" + }, + "pageNumbers": { + "name": "页码", + "subtitle": "将页码插入到您的文档中。" + }, + "addWatermark": { + "name": "添加水印", + "subtitle": "在您的 PDF 页面上添加文字或图片水印。" + }, + "headerFooter": { + "name": "页眉和页脚", + "subtitle": "在页面顶部和底部添加文字。" + }, + "invertColors": { + "name": "反转颜色", + "subtitle": "创建您的 PDF 的“暗黑模式”版本。" + }, + "backgroundColor": { + "name": "背景颜色", + "subtitle": "更改您的 PDF 的背景颜色。" + }, + "changeTextColor": { + "name": "更改文本颜色", + "subtitle": "更改您 PDF 中文本的颜色。" + }, + "addStamps": { + "name": "添加印章", + "subtitle": "使用注释工具栏向您的 PDF 添加图片印章。" + }, + "removeAnnotations": { + "name": "移除注释", + "subtitle": "移除评论、高亮和链接。" + }, + "pdfFormFiller": { + "name": "PDF 表单填写器", + "subtitle": "直接在浏览器中填写表单。也支持 XFA 表单。" + }, + "createPdfForm": { + "name": "创建 PDF 表单", + "subtitle": "使用拖放文本字段创建可填写的 PDF 表单。" + }, + "removeBlankPages": { + "name": "移除空白页", + "subtitle": "自动检测并删除空白页。" + }, + "imageToPdf": { + "name": "图片转 PDF", + "subtitle": "将 JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP 转换为 PDF。" + }, + "pngToPdf": { + "name": "PNG 转 PDF", + "subtitle": "从一张或多张 PNG 图片创建 PDF。" + }, + "webpToPdf": { + "name": "WebP 转 PDF", + "subtitle": "从一张或多张 WebP 图片创建 PDF。" + }, + "svgToPdf": { + "name": "SVG 转 PDF", + "subtitle": "从一张或多张 SVG 图片创建 PDF。" + }, + "bmpToPdf": { + "name": "BMP 转 PDF", + "subtitle": "从一张或多张 BMP 图片创建 PDF。" + }, + "heicToPdf": { + "name": "HEIC 转 PDF", + "subtitle": "从一张或多张 HEIC 图片创建 PDF。" + }, + "tiffToPdf": { + "name": "TIFF 转 PDF", + "subtitle": "从一张或多张 TIFF 图片创建 PDF。" + }, + "textToPdf": { + "name": "文本转 PDF", + "subtitle": "将纯文本文件转换为 PDF。" + }, + "jsonToPdf": { + "name": "JSON 转 PDF", + "subtitle": "将 JSON 文件转换为 PDF 格式。" + }, + "pdfToJpg": { + "name": "PDF 转 JPG", + "subtitle": "将每一页 PDF 转换为 JPG 图片。" + }, + "pdfToPng": { + "name": "PDF 转 PNG", + "subtitle": "将每一页 PDF 转换为 PNG 图片。" + }, + "pdfToWebp": { + "name": "PDF 转 WebP", + "subtitle": "将每一页 PDF 转换为 WebP 图片。" + }, + "pdfToBmp": { + "name": "PDF 转 BMP", + "subtitle": "将每一页 PDF 转换为 BMP 图片。" + }, + "pdfToTiff": { + "name": "PDF 转 TIFF", + "subtitle": "将每一页 PDF 转换为 TIFF 图片。" + }, + "pdfToGreyscale": { + "name": "PDF 转 灰度", + "subtitle": "将所有颜色转换为黑白。" + }, + "pdfToJson": { + "name": "PDF 转 JSON", + "subtitle": "将 PDF 文件转换为 JSON 格式。" + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "使 PDF 可搜索和可复制。" + }, + "alternateMix": { + "name": "交替混合页面", + "subtitle": "通过交替每个 PDF 的页面来合并 PDF。保留书签。" + }, + "addAttachments": { + "name": "添加附件", + "subtitle": "将一个或多个文件嵌入到您的 PDF 中。" + }, + "extractAttachments": { + "name": "提取附件", + "subtitle": "从 PDF 中提取所有嵌入的文件为 ZIP。" + }, + "editAttachments": { + "name": "编辑附件", + "subtitle": "查看或移除 PDF 中的附件。" + }, + "dividePages": { + "name": "分割页面", + "subtitle": "水平或垂直分割页面。" + }, + "addBlankPage": { + "name": "添加空白页", + "subtitle": "在 PDF 的任意位置插入空白页。" + }, + "reversePages": { + "name": "反转页面", + "subtitle": "反转文档中所有页面的顺序。" + }, + "rotatePdf": { + "name": "旋转 PDF", + "subtitle": "以 90 度增量旋转页面。" + }, + "rotateCustom": { + "name": "按自定义角度旋转", + "subtitle": "按任意自定义角度旋转页面。" + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "将多页排列在单张纸上。" + }, + "combineToSinglePage": { + "name": "合并为单页", + "subtitle": "将所有页面拼接成一个连续的滚动页面。" + }, + "viewMetadata": { + "name": "查看元数据", + "subtitle": "检查 PDF 的隐藏属性。" + }, + "editMetadata": { + "name": "编辑元数据", + "subtitle": "更改作者、标题和其他属性。" + }, + "pdfsToZip": { + "name": "PDF 转 ZIP", + "subtitle": "将多个 PDF 文件打包成一个 ZIP 归档。" + }, + "comparePdfs": { + "name": "比较 PDF", + "subtitle": "并排比较两个 PDF。" + }, + "posterizePdf": { + "name": "海报化 PDF", + "subtitle": "将大页面分割成多个小页面。" + }, + "fixPageSize": { + "name": "修复页面尺寸", + "subtitle": "将所有页面标准化为统一尺寸。" + }, + "linearizePdf": { + "name": "线性化 PDF", + "subtitle": "优化 PDF 以便快速网络查看。" + }, + "pageDimensions": { + "name": "页面尺寸", + "subtitle": "分析页面大小、方向和单位。" + }, + "removeRestrictions": { + "name": "移除限制", + "subtitle": "移除与数字签名 PDF 文件相关的密码保护和安全限制。" + }, + "repairPdf": { + "name": "修复 PDF", + "subtitle": "从损坏的 PDF 文件中恢复数据。" + }, + "encryptPdf": { + "name": "加密 PDF", + "subtitle": "通过添加密码锁定您的 PDF。" + }, + "sanitizePdf": { + "name": "清理 PDF", + "subtitle": "移除元数据、注释、脚本等。" + }, + "decryptPdf": { + "name": "解密 PDF", + "subtitle": "通过移除密码保护解锁 PDF。" + }, + "flattenPdf": { + "name": "扁平化 PDF", + "subtitle": "使表单字段和注释不可编辑。" + }, + "removeMetadata": { + "name": "移除元数据", + "subtitle": "从 PDF 中剥离隐藏数据。" + }, + "changePermissions": { + "name": "更改权限", + "subtitle": "设置或更改 PDF 上的用户权限。" + }, + "odtToPdf": { + "name": "ODT 转 PDF", + "subtitle": "将 OpenDocument 文本文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "ODT 文件", + "convertButton": "转换为 PDF" + }, + "csvToPdf": { + "name": "CSV 转 PDF", + "subtitle": "将 CSV 电子表格文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "CSV 件", + "convertButton": "转换为 PDF" + }, + "rtfToPdf": { + "name": "RTF 转 PDF", + "subtitle": "将富文本格式文档转换为 PDF。支持多个文件。", + "acceptedFormats": "RTF 文件", + "convertButton": "转换为 PDF" + }, + "wordToPdf": { + "name": "Word 转 PDF", + "subtitle": "将 Word 文档 (DOCX, DOC, ODT, RTF) 转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "DOCX, DOC, ODT, RTF 文件", + "convertButton": "转换为 PDF" + }, + "excelToPdf": { + "name": "Excel 转 PDF", + "subtitle": "将 Excel 电子表格 (XLSX, XLS, ODS, CSV) 转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "XLSX, XLS, ODS, CSV 文件", + "convertButton": "转换为 PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint 转 PDF", + "subtitle": "将 PowerPoint 演示文稿 (PPTX, PPT, ODP) 转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "PPTX, PPT, ODP 文件", + "convertButton": "转换为 PDF" + }, + "markdownToPdf": { + "name": "Markdown 转 PDF", + "subtitle": "编写或粘贴 Markdown 并将其导出为精美格式的 PDF。", + "paneMarkdown": "Markdown", + "panePreview": "预览", + "btnUpload": "上传", + "btnSyncScroll": "同步滚动", + "btnSettings": "设置", + "btnExportPdf": "导出 PDF", + "settingsTitle": "Markdown 设置", + "settingsPreset": "预设", + "presetDefault": "默认 (GFM 风格)", + "presetCommonmark": "CommonMark (严格)", + "presetZero": "最小 (无功能)", + "settingsOptions": "Markdown 选项", + "optAllowHtml": "允许 HTML 标签", + "optBreaks": "将换行转换为
", + "optLinkify": "自动将 URL 转换为链接", + "optTypographer": "排版器 (智能引号等)" + }, + "pdfBooklet": { + "name": "PDF 小册子", + "subtitle": "重新排列页面用于双面小册子打印。折叠并装订以创建小册子。", + "howItWorks": "工作原理:", + "step1": "上传 PDF 文件。", + "step2": "页面将按小册子顺序重新排列。", + "step3": "双面打印,短边翻转,折叠并装订。", + "paperSize": "纸张大小", + "orientation": "方向", + "portrait": "纵向", + "landscape": "横向", + "pagesPerSheet": "每张页数", + "createBooklet": "创建小册子", + "processing": "处理中...", + "pageCount": "如需要,页数将补齐为 4 的倍数。" + }, + "xpsToPdf": { + "name": "XPS 转 PDF", + "subtitle": "将 XPS/OXPS 文档转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "XPS, OXPS 文件", + "convertButton": "转换为 PDF" + }, + "mobiToPdf": { + "name": "MOBI 转 PDF", + "subtitle": "将 MOBI 电子书转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "MOBI 文件", + "convertButton": "转换为 PDF" + }, + "epubToPdf": { + "name": "EPUB 转 PDF", + "subtitle": "将 EPUB 电子书转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "EPUB 文件", + "convertButton": "转换为 PDF" + }, + "fb2ToPdf": { + "name": "FB2 转 PDF", + "subtitle": "将 FictionBook (FB2) 电子书转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "FB2 文件", + "convertButton": "转换为 PDF" + }, + "cbzToPdf": { + "name": "CBZ 转 PDF", + "subtitle": "将漫画档案 (CBZ/CBR) 转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "CBZ, CBR 文件", + "convertButton": "转换为 PDF" + }, + "wpdToPdf": { + "name": "WPD 转 PDF", + "subtitle": "将 WordPerfect 文档 (WPD) 转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "WPD 文件", + "convertButton": "转换为 PDF" + }, + "wpsToPdf": { + "name": "WPS 转 PDF", + "subtitle": "将 WPS Office 文档转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "WPS 文件", + "convertButton": "转换为 PDF" + }, + "xmlToPdf": { + "name": "XML 转 PDF", + "subtitle": "将 XML 文档转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "XML 文件", + "convertButton": "转换为 PDF" + }, + "pagesToPdf": { + "name": "Pages 转 PDF", + "subtitle": "将 Apple Pages 文档转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "Pages 文件", + "convertButton": "转换为 PDF" + }, + "odgToPdf": { + "name": "ODG 转 PDF", + "subtitle": "将 OpenDocument Graphics (ODG) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "ODG 文件", + "convertButton": "转换为 PDF" + }, + "odsToPdf": { + "name": "ODS 转 PDF", + "subtitle": "将 OpenDocument Spreadsheet (ODS) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "ODS 文件", + "convertButton": "转换为 PDF" + }, + "odpToPdf": { + "name": "ODP 转 PDF", + "subtitle": "将 OpenDocument Presentation (ODP) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "ODP 文件", + "convertButton": "转换为 PDF" + }, + "pubToPdf": { + "name": "PUB 转 PDF", + "subtitle": "将 Microsoft Publisher (PUB) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "PUB 文件", + "convertButton": "转换为 PDF" + }, + "vsdToPdf": { + "name": "VSD 转 PDF", + "subtitle": "将 Microsoft Visio (VSD, VSDX) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "VSD, VSDX 文件", + "convertButton": "转换为 PDF" + }, + "psdToPdf": { + "name": "PSD 转 PDF", + "subtitle": "将 Adobe Photoshop (PSD) 文件转换为 PDF 格式。支持多个文件。", + "acceptedFormats": "PSD 文件", + "convertButton": "转换为 PDF" + }, + "pdfToSvg": { + "name": "PDF 转 SVG", + "subtitle": "将 PDF 文件的每一页转换为可缩放矢量图形 (SVG),在任何尺寸下都能保持完美质量。" + }, + "extractTables": { + "name": "提取 PDF 表格", + "subtitle": "从 PDF 文件中提取表格,并导出为 CSV、JSON 或 Markdown 格式。" + }, + "pdfToCsv": { + "name": "PDF 转 CSV", + "subtitle": "从 PDF 中提取表格并转换为 CSV 格式。" + }, + "pdfToExcel": { + "name": "PDF 转 Excel", + "subtitle": "从 PDF 中提取表格并转换为 Excel (XLSX) 格式。" + }, + "pdfToText": { + "name": "PDF 转 文本", + "subtitle": "从 PDF 文件中提取文本并保存为纯文本文件 (.txt)。支持多个文件。", + "note": "此工具仅适用于数字创建的 PDF。对于扫描文档或基于图像的 PDF,请使用我们的 OCR PDF 工具。", + "convertButton": "提取文本" + }, + "digitalSignPdf": { + "name": "PDF 数字签名", + "pageTitle": "PDF 数字签名 - 添加加密签名 | BentoPDF", + "subtitle": "使用 X.509 证书为您的 PDF 添加加密数字签名。支持 PKCS#12 (.pfx, .p12) 和 PEM 格式。您的私钥永远不会离开您的浏览器。", + "certificateSection": "证书", + "uploadCert": "上传证书 (.pfx, .p12)", + "certPassword": "证书密码", + "certPasswordPlaceholder": "输入证书密码", + "certInfo": "证书信息", + "certSubject": "主体", + "certIssuer": "颁发者", + "certValidity": "有效期", + "signatureDetails": "签名详情(可选)", + "reason": "原因", + "reasonPlaceholder": "例如:我批准此文档", + "location": "位置", + "locationPlaceholder": "例如:北京,中国", + "contactInfo": "联系信息", + "contactPlaceholder": "例如:email@example.com", + "applySignature": "应用数字签名", + "successMessage": "PDF 签名成功!签名可在任何 PDF 阅读器中验证。" + }, + "validateSignaturePdf": { + "name": "验证 PDF 签名", + "pageTitle": "验证 PDF 签名 - 验证数字签名 | BentoPDF", + "subtitle": "验证您的 PDF 文件中的数字签名。检查证书有效性、查看签名者详情并确认文档完整性。所有处理都在您的浏览器中进行。" + }, + "emailToPdf": { + "name": "邮件转 PDF", + "subtitle": "将电子邮件文件 (EML, MSG) 转换为 PDF 格式。支持 Outlook 导出和标准邮件格式。", + "acceptedFormats": "EML, MSG 文件", + "convertButton": "转换为 PDF" + } +} diff --git a/public/sitemap.xml b/public/sitemap.xml index ced6136cc..7613a80aa 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -281,6 +281,12 @@ monthly 0.5 + + https://www.bentopdf.com/email-to-pdf + 2026-01-08 + monthly + 0.7 + https://www.bentopdf.com/xps-to-pdf 2024-12-28 diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 53be8165e..9720e186b 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -7,7 +7,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'pdf-multi-tool.html', name: 'PDF Multi Tool', icon: 'ph-pencil-ruler', - subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.', + subtitle: + 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.', }, { href: import.meta.env.BASE_URL + 'merge-pdf.html', @@ -158,7 +159,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'form-filler.html', name: 'PDF Form Filler', icon: 'ph-pencil-line', - subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.', + subtitle: + 'Fill in forms directly in the browser. Also supports XFA forms.', }, { href: import.meta.env.BASE_URL + 'form-creator.html', @@ -181,7 +183,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'image-to-pdf.html', name: 'Images to PDF', icon: 'ph-images', - subtitle: 'Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP to PDF.', + subtitle: + 'Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP to PDF.', }, { href: import.meta.env.BASE_URL + 'jpg-to-pdf.html', @@ -235,7 +238,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'markdown-to-pdf.html', name: 'Markdown to PDF', icon: 'ph-markdown-logo', - subtitle: 'Convert Markdown to PDF with live preview and syntax highlighting.', + subtitle: + 'Convert Markdown to PDF with live preview and syntax highlighting.', }, { href: import.meta.env.BASE_URL + 'json-to-pdf.html', @@ -367,7 +371,14 @@ export const categories = [ href: import.meta.env.BASE_URL + 'psd-to-pdf.html', name: 'PSD to PDF', icon: 'ph-image', - subtitle: 'Convert Adobe Photoshop (PSD) files to PDF. Multiple files supported.', + subtitle: + 'Convert Adobe Photoshop (PSD) files to PDF. Multiple files supported.', + }, + { + href: import.meta.env.BASE_URL + 'email-to-pdf.html', + name: 'Email to PDF', + icon: 'ph-envelope', + subtitle: 'Convert email files (EML, MSG) to PDF format.', }, ], }, @@ -456,7 +467,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'prepare-pdf-for-ai.html', name: 'Prepare PDF for AI', icon: 'ph-sparkle', - subtitle: 'Extract PDF content as LlamaIndex JSON for RAG/LLM pipelines.', + subtitle: + 'Extract PDF content as LlamaIndex JSON for RAG/LLM pipelines.', }, { href: import.meta.env.BASE_URL + 'pdf-to-text.html', @@ -485,7 +497,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'alternate-merge.html', name: 'Alternate & Mix Pages', icon: 'ph-shuffle', - subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks', + subtitle: + 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks', }, { href: import.meta.env.BASE_URL + 'organize-pdf.html', @@ -677,7 +690,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'rasterize-pdf.html', name: 'Rasterize PDF', icon: 'ph-image', - subtitle: 'Convert PDF to image-based PDF. Flatten layers and remove selectable text.', + subtitle: + 'Convert PDF to image-based PDF. Flatten layers and remove selectable text.', }, ], }, @@ -724,7 +738,8 @@ export const categories = [ href: import.meta.env.BASE_URL + 'digital-sign-pdf.html', name: 'Digital Signature', icon: 'ph-certificate', - subtitle: 'Add a cryptographic digital signature using X.509 certificates.', + subtitle: + 'Add a cryptographic digital signature using X.509 certificates.', }, { href: import.meta.env.BASE_URL + 'validate-signature-pdf.html', diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index acc7f958a..ed104643d 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -3,29 +3,45 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // Supported languages -export const supportedLanguages = ['en', 'de', 'zh', 'vi', 'tr'] as const; +export const supportedLanguages = [ + 'en', + 'de', + 'zh', + 'vi', + 'tr', + 'id', + 'it', +] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { - en: 'English', - de: 'Deutsch', - zh: '中文', - vi: 'Tiếng Việt', - tr: 'Türkçe', + en: 'English', + de: 'Deutsch', + zh: '中文', + vi: 'Tiếng Việt', + tr: 'Türkçe', + id: 'Bahasa Indonesia', + it: 'Italiano', }; export const getLanguageFromUrl = (): SupportedLanguage => { - const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi|tr)(?:\/|$)/); - if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) { - return langMatch[1] as SupportedLanguage; - } - const storedLang = localStorage.getItem('i18nextLng'); - if (storedLang && supportedLanguages.includes(storedLang as SupportedLanguage)) { - return storedLang as SupportedLanguage; - } - - return 'en'; + const path = window.location.pathname; + const langMatch = path.match(/^\/(en|de|zh|vi|tr|id|it)(?:\/|$)/); + if ( + langMatch && + supportedLanguages.includes(langMatch[1] as SupportedLanguage) + ) { + return langMatch[1] as SupportedLanguage; + } + const storedLang = localStorage.getItem('i18nextLng'); + if ( + storedLang && + supportedLanguages.includes(storedLang as SupportedLanguage) + ) { + return storedLang as SupportedLanguage; + } + + return 'en'; }; let initialized = false; @@ -66,22 +82,22 @@ export const t = (key: string, options?: Record): string => { }; export const changeLanguage = (lang: SupportedLanguage): void => { - if (!supportedLanguages.includes(lang)) return; - - const currentPath = window.location.pathname; - const currentLang = getLanguageFromUrl(); + if (!supportedLanguages.includes(lang)) return; - let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi|tr)\//)) { - newPath = currentPath.replace(/^\/(en|de|zh|vi|tr)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|zh|vi|tr)$/)) { - newPath = `/${lang}`; - } else { - newPath = `/${lang}${currentPath}`; - } + const currentPath = window.location.pathname; + const currentLang = getLanguageFromUrl(); - const newUrl = newPath + window.location.search + window.location.hash; - window.location.href = newUrl; + let newPath: string; + if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { + newPath = currentPath.replace(/^\/(en|de|zh|vi|tr|id|it)\//, `/${lang}/`); + } else if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)$/)) { + newPath = `/${lang}`; + } else { + newPath = `/${lang}${currentPath}`; + } + + const newUrl = newPath + window.location.search + window.location.hash; + window.location.href = newUrl; }; // Apply translations to all elements with data-i18n attribute @@ -120,38 +136,40 @@ export const applyTranslations = (): void => { }; export const rewriteLinks = (): void => { - const currentLang = getLanguageFromUrl(); - if (currentLang === 'en') return; - - const links = document.querySelectorAll('a[href]'); - links.forEach((link) => { - const href = link.getAttribute('href'); - if (!href) return; - - if (href.startsWith('http') || - href.startsWith('mailto:') || - href.startsWith('tel:') || - href.startsWith('#') || - href.startsWith('javascript:')) { - return; - } - - if (href.match(/^\/(en|de|zh|vi|tr|id)\//)) { - return; - } - let newHref: string; - if (href.startsWith('/')) { - newHref = `/${currentLang}${href}`; - } else if (href.startsWith('./')) { - newHref = href.replace('./', `/${currentLang}/`); - } else if (href === '/' || href === '') { - newHref = `/${currentLang}/`; - } else { - newHref = `/${currentLang}/${href}`; - } - - link.setAttribute('href', newHref); - }); + const currentLang = getLanguageFromUrl(); + if (currentLang === 'en') return; + + const links = document.querySelectorAll('a[href]'); + links.forEach((link) => { + const href = link.getAttribute('href'); + if (!href) return; + + if ( + href.startsWith('http') || + href.startsWith('mailto:') || + href.startsWith('tel:') || + href.startsWith('#') || + href.startsWith('javascript:') + ) { + return; + } + + if (href.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { + return; + } + let newHref: string; + if (href.startsWith('/')) { + newHref = `/${currentLang}${href}`; + } else if (href.startsWith('./')) { + newHref = href.replace('./', `/${currentLang}/`); + } else if (href === '/' || href === '') { + newHref = `/${currentLang}/`; + } else { + newHref = `/${currentLang}/${href}`; + } + + link.setAttribute('href', newHref); + }); }; export default i18next; diff --git a/src/js/logic/email-to-pdf-page.ts b/src/js/logic/email-to-pdf-page.ts new file mode 100644 index 000000000..09d856db6 --- /dev/null +++ b/src/js/logic/email-to-pdf-page.ts @@ -0,0 +1,268 @@ +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { state } from '../state.js'; +import { createIcons, icons } from 'lucide'; +import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js'; +import { PyMuPDF } from '@bentopdf/pymupdf-wasm'; +import { getWasmBaseUrl } from '../config/wasm-cdn-config.js'; + +const EXTENSIONS = ['.eml', '.msg']; +const TOOL_NAME = 'Email'; + +document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } + + const convertOptions = document.getElementById('convert-options'); + + const updateUI = async () => { + if (!fileDisplayArea || !processBtn || !fileControls) return; + + if (state.files.length > 0) { + fileDisplayArea.innerHTML = ''; + + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); + + infoContainer.append(nameSpan, metaSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + } + + createIcons({ icons }); + fileControls.classList.remove('hidden'); + if (convertOptions) convertOptions.classList.remove('hidden'); + (processBtn as HTMLButtonElement).disabled = false; + } else { + fileDisplayArea.innerHTML = ''; + fileControls.classList.add('hidden'); + if (convertOptions) convertOptions.classList.add('hidden'); + (processBtn as HTMLButtonElement).disabled = true; + } + }; + + const resetState = () => { + state.files = []; + state.pdfDoc = null; + updateUI(); + }; + + const convertToPdf = async () => { + try { + if (state.files.length === 0) { + showAlert( + 'No Files', + `Please select at least one ${TOOL_NAME} file (.eml or .msg).` + ); + return; + } + + const pageSizeSelect = document.getElementById( + 'page-size' + ) as HTMLSelectElement; + const includeCcBccCheckbox = document.getElementById( + 'include-cc-bcc' + ) as HTMLInputElement; + const includeAttachmentsCheckbox = document.getElementById( + 'include-attachments' + ) as HTMLInputElement; + + const pageSize = + (pageSizeSelect?.value as 'a4' | 'letter' | 'legal') || 'a4'; + const includeCcBcc = includeCcBccCheckbox?.checked ?? true; + const includeAttachments = includeAttachmentsCheckbox?.checked ?? true; + + showLoader('Loading PDF engine...'); + const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf')); + await pymupdf.load(); + + if (state.files.length === 1) { + const originalFile = state.files[0]; + showLoader(`Parsing ${originalFile.name}...`); + + const email = await parseEmailFile(originalFile); + + showLoader('Generating PDF...'); + const htmlContent = renderEmailToHtml(email, { + includeCcBcc, + includeAttachments, + pageSize, + }); + + const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, { + pageSize, + margins: { top: 50, right: 50, bottom: 50, left: 50 }, + attachments: email.attachments + .filter((a) => a.content) + .map((a) => ({ + filename: a.filename, + content: a.content!, + })), + }); + const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf'; + + downloadFile(pdfBlob, fileName); + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${originalFile.name} to PDF.`, + 'success', + () => resetState() + ); + } else { + showLoader('Converting emails...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + showLoader( + `Converting ${i + 1}/${state.files.length}: ${file.name}...` + ); + + try { + const email = await parseEmailFile(file); + + const htmlContent = renderEmailToHtml(email, { + includeCcBcc, + includeAttachments, + pageSize, + }); + + const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, { + pageSize, + margins: { top: 50, right: 50, bottom: 50, left: 50 }, + attachments: email.attachments + .filter((a) => a.content) + .map((a) => ({ + filename: a.filename, + content: a.content!, + })), + }); + const baseName = file.name.replace(/\.[^.]+$/, ''); + const pdfBuffer = await pdfBlob.arrayBuffer(); + zip.file(`${baseName}.pdf`, pdfBuffer); + } catch (e: any) { + console.error(`Failed to convert ${file.name}:`, e); + } + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'emails-converted.zip'); + + hideLoader(); + + showAlert( + 'Conversion Complete', + `Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`, + 'success', + () => resetState() + ); + } + } catch (e: any) { + console.error(`[${TOOL_NAME}2PDF] ERROR:`, e); + hideLoader(); + showAlert( + 'Error', + `An error occurred during conversion. Error: ${e.message}` + ); + } + }; + + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const validFiles = Array.from(files).filter((f) => { + const name = f.name.toLowerCase(); + return EXTENSIONS.some((ext) => name.endsWith(ext)); + }); + if (validFiles.length > 0) { + const dataTransfer = new DataTransfer(); + validFiles.forEach((f) => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); + + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convertToPdf); + } +}); diff --git a/src/js/logic/email-to-pdf.ts b/src/js/logic/email-to-pdf.ts new file mode 100644 index 000000000..378f1d96a --- /dev/null +++ b/src/js/logic/email-to-pdf.ts @@ -0,0 +1,393 @@ +import PostalMime from 'postal-mime'; +import MsgReader from '@kenjiuno/msgreader'; +import { formatBytes, escapeHtml } from '../utils/helpers.js'; +import type { EmailAttachment, ParsedEmail, EmailRenderOptions } from '@/types'; + +// Re-export types for convenience +export type { EmailAttachment, ParsedEmail, EmailRenderOptions }; + +/** + * Format email address without angle brackets for cleaner display + */ +function formatAddress( + name: string | undefined, + email: string | undefined +): string { + if (name && email) { + return `${name} (${email})`; + } + return email || name || ''; +} + +export async function parseEmlFile(file: File): Promise { + const arrayBuffer = await file.arrayBuffer(); + const parser = new PostalMime(); + const email = await parser.parse(arrayBuffer); + + const from = + formatAddress(email.from?.name, email.from?.address) || 'Unknown Sender'; + + const to = (email.to || []) + .map((addr) => formatAddress(addr.name, addr.address)) + .filter(Boolean); + + const cc = (email.cc || []) + .map((addr) => formatAddress(addr.name, addr.address)) + .filter(Boolean); + + const bcc = (email.bcc || []) + .map((addr) => formatAddress(addr.name, addr.address)) + .filter(Boolean); + + // Helper to map parsing result to EmailAttachment + const mapAttachment = (att: any): EmailAttachment => { + let content: Uint8Array | undefined; + let size = 0; + if (att.content) { + if (att.content instanceof ArrayBuffer) { + content = new Uint8Array(att.content); + size = content.byteLength; + } else if (att.content instanceof Uint8Array) { + content = att.content; + size = content.byteLength; + } + } + return { + filename: att.filename || 'unnamed', + size, + contentType: att.mimeType || 'application/octet-stream', + content, + contentId: att.contentId + ? att.contentId.replace(/^<|>$/g, '') + : undefined, + }; + }; + + const attachments: EmailAttachment[] = [ + ...(email.attachments || []).map(mapAttachment), + ...((email as any).inline || []).map(mapAttachment), + ]; + + // Preserve original date string from headers + let rawDateString = ''; + if (email.headers) { + const dateHeader = email.headers.find( + (h) => h.key.toLowerCase() === 'date' + ); + if (dateHeader) { + rawDateString = dateHeader.value as string; + } + } + if (!rawDateString && email.date) { + rawDateString = email.date; // fallback if header missing but parsed date exists as string? + } + + let parsedDate: Date | null = null; + if (email.date) { + try { + parsedDate = new Date(email.date); + if (isNaN(parsedDate.getTime())) { + parsedDate = null; + } + } catch { + parsedDate = null; + } + } + + return { + subject: email.subject || '(No Subject)', + from, + to, + cc, + bcc, + date: parsedDate, + rawDateString, + htmlBody: email.html || '', + textBody: email.text || '', + attachments, + }; +} + +export async function parseMsgFile(file: File): Promise { + const arrayBuffer = await file.arrayBuffer(); + const msgReader = new MsgReader(arrayBuffer); + const msgData = msgReader.getFileData(); + + const from = + formatAddress(msgData.senderName, msgData.senderEmail) || 'Unknown Sender'; + + const to: string[] = []; + const cc: string[] = []; + const bcc: string[] = []; + + if (msgData.recipients) { + for (const recipient of msgData.recipients) { + const recipientStr = formatAddress(recipient.name, recipient.email); + if (!recipientStr) continue; + + const recipType = String(recipient.recipType).toLowerCase(); + if (recipType === 'cc' || recipType === '2') { + cc.push(recipientStr); + } else if (recipType === 'bcc' || recipType === '3') { + bcc.push(recipientStr); + } else { + to.push(recipientStr); + } + } + } + + const attachments: EmailAttachment[] = (msgData.attachments || []).map( + (att: any) => ({ + filename: att.fileName || att.name || 'unnamed', + size: att.content?.length || 0, + contentType: att.mimeType || 'application/octet-stream', + content: att.content ? new Uint8Array(att.content) : undefined, + contentId: att.pidContentId + ? att.pidContentId.replace(/^<|>$/g, '') + : undefined, + }) + ); + + let date: Date | null = null; + let rawDateString = ''; + if (msgData.messageDeliveryTime) { + rawDateString = msgData.messageDeliveryTime; + date = new Date(msgData.messageDeliveryTime); + } else if (msgData.clientSubmitTime) { + rawDateString = msgData.clientSubmitTime; + date = new Date(msgData.clientSubmitTime); + } + + return { + subject: msgData.subject || '(No Subject)', + from, + to, + cc, + bcc, + date, + rawDateString, + htmlBody: msgData.bodyHtml || '', + textBody: msgData.body || '', + attachments, + }; +} + +/** + * Formats a raw RFC 2822 date string into a nicer human-readable format, + * while preserving the original timezone and time. + * Example input: "Sun, 8 Jan 2017 20:37:44 +0200" + * Example output: "Sunday, January 8, 2017 at 8:37 PM (+0200)" + */ +function formatRawDate(raw: string): string { + try { + // Regex to parse RFC 2822 date parts: Day, DD Mon YYYY HH:MM:SS Timezone + const match = raw.match( + /([A-Za-z]{3}),\s+(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s+([+-]\d{4})/ + ); + + if (match) { + const [ + , + dayAbbr, + dom, + monthAbbr, + year, + hoursStr, + minsStr, + secsStr, + timezone, + ] = match; + + // Map abbreviations to full names + const days: Record = { + Sun: 'Sunday', + Mon: 'Monday', + Tue: 'Tuesday', + Wed: 'Wednesday', + Thu: 'Thursday', + Fri: 'Friday', + Sat: 'Saturday', + }; + const months: Record = { + Jan: 'January', + Feb: 'February', + Mar: 'March', + Apr: 'April', + May: 'May', + Jun: 'June', + Jul: 'July', + Aug: 'August', + Sep: 'September', + Oct: 'October', + Nov: 'November', + Dec: 'December', + }; + + const fullDay = days[dayAbbr] || dayAbbr; + const fullMonth = months[monthAbbr] || monthAbbr; + + // Convert to 12-hour format manually + let hours = parseInt(hoursStr, 10); + const ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + + // Format timezone: +0200 -> UTC+02:00 + const tzSign = timezone.substring(0, 1); + const tzHours = timezone.substring(1, 3); + const tzMins = timezone.substring(3, 5); + const formattedTz = `UTC${tzSign}${tzHours}:${tzMins}`; + + return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`; + } + } catch (e) { + // Fallback to raw string if parsing fails + } + return raw; +} + +/** + * Replace CID references in HTML with base64 data URIs + */ +function processInlineImages( + html: string, + attachments: EmailAttachment[] +): string { + if (!html) return html; + + // Create a map of contentIds to attachments + const cidMap = new Map(); + attachments.forEach((att) => { + if (att.contentId) { + cidMap.set(att.contentId, att); + } + }); + + // Replace src="cid:..." + return html.replace(/src=["']cid:([^"']+)["']/g, (match, cid) => { + const att = cidMap.get(cid); + if (att && att.content) { + // Convert Uint8Array to base64 + let binary = ''; + const len = att.content.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(att.content[i]); + } + const base64 = + typeof btoa === 'function' + ? btoa(binary) + : Buffer.from(binary, 'binary').toString('base64'); + return `src="data:${att.contentType};base64,${base64}"`; + } + return match; // Keep original if not found + }); +} + +export function renderEmailToHtml( + email: ParsedEmail, + options: EmailRenderOptions = {} +): string { + const { includeCcBcc = true, includeAttachments = true } = options; + + let processedHtml = ''; + if (email.htmlBody) { + processedHtml = processInlineImages(email.htmlBody, email.attachments); + } else { + processedHtml = `
${escapeHtml(email.textBody)}
`; + } + + // Format date in a human-readable way + let dateStr = 'Unknown Date'; + if (email.rawDateString) { + dateStr = formatRawDate(email.rawDateString); + } else if (email.date && !isNaN(email.date.getTime())) { + dateStr = email.date.toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + } + + const attachmentHtml = + includeAttachments && email.attachments.length > 0 + ? ` +
+

Attachments (${email.attachments.length})

+
    + ${email.attachments + .map( + (att) => + `
  • ${escapeHtml(att.filename)} (${formatBytes(att.size)})
  • ` + ) + .join('')} +
+
+ ` + : ''; + + // Build CC/BCC rows + let ccBccHtml = ''; + if (includeCcBcc) { + if (email.cc.length > 0) { + ccBccHtml += ` +
+ CC: + ${escapeHtml(email.cc.join(', '))} +
`; + } + if (email.bcc.length > 0) { + ccBccHtml += ` +
+ BCC: + ${escapeHtml(email.bcc.join(', '))} +
`; + } + } + + return ` + + + + + +
+

${escapeHtml(email.subject)}

+
+
+ From: + ${escapeHtml(email.from)} +
+
+ To: + ${escapeHtml(email.to.join(', ') || 'Unknown')} +
+ ${ccBccHtml} +
+ Date: + ${escapeHtml(dateStr)} +
+
+
+
+ ${processedHtml} +
+ ${attachmentHtml} + +`; +} + +export async function parseEmailFile(file: File): Promise { + const ext = file.name.toLowerCase().split('.').pop(); + + if (ext === 'eml') { + return parseEmlFile(file); + } else if (ext === 'msg') { + return parseMsgFile(file); + } else { + throw new Error(`Unsupported file type: .${ext}`); + } +} diff --git a/src/js/main.ts b/src/js/main.ts index c56ef1446..74bbf2870 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -270,6 +270,7 @@ const init = async () => { 'Flatten PDF': 'tools:flattenPdf', 'Remove Metadata': 'tools:removeMetadata', 'Change Permissions': 'tools:changePermissions', + 'Email to PDF': 'tools:emailToPdf', }; // Homepage-only tool grid rendering (not used on individual tool pages) diff --git a/src/js/types/email-to-pdf-type.ts b/src/js/types/email-to-pdf-type.ts new file mode 100644 index 000000000..89d81668e --- /dev/null +++ b/src/js/types/email-to-pdf-type.ts @@ -0,0 +1,26 @@ +export interface EmailAttachment { + filename: string; + size: number; + contentType: string; + content?: Uint8Array; + contentId?: string; +} + +export interface ParsedEmail { + subject: string; + from: string; + to: string[]; + cc: string[]; + bcc: string[]; + date: Date | null; + rawDateString: string; + htmlBody: string; + textBody: string; + attachments: EmailAttachment[]; +} + +export interface EmailRenderOptions { + includeCcBcc?: boolean; + includeAttachments?: boolean; + pageSize?: 'a4' | 'letter' | 'legal'; +} diff --git a/src/js/types/index.ts b/src/js/types/index.ts index 9f73240ea..e20d2c109 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -1,48 +1,48 @@ -export * from './ocr-type.js'; -export * from './form-creator-type.js'; -export * from './digital-sign-type.js'; -export * from './attachment-type.js'; -export * from './edit-attachments-type.js'; -export * from './edit-metadata-type.js'; -export * from './divide-pages-type.js'; -export * from './alternate-merge-page-type.js'; -export * from './add-blank-page-type.js'; -export * from './compare-pdfs-type.js'; -export * from './fix-page-size-type.js'; -export * from './view-metadata-type.js'; -export * from './header-footer-type.js'; -export * from './encrypt-pdf-type.js'; -export * from './flatten-pdf-type.js'; -export * from './crop-pdf-type.js'; -export * from './background-color-type.js'; -export * from './posterize-type.js'; -export * from './decrypt-pdf-type.js'; -export * from './combine-single-page-type.js'; -export * from './change-permissions-type.js'; -export * from './validate-signature-type.js'; -export * from './remove-restrictions-type.js'; -export * from './page-dimensions-type.js'; -export * from './extract-attachments-type.js'; -export * from './pdf-multi-tool-type.js'; -export * from './ocr-pdf-type.js'; -export * from './delete-pages-type.js'; -export * from './invert-colors-type.js'; -export * from './table-of-contents-type.js'; -export * from './organize-pdf-type.js'; -export * from './merge-pdf-type.js'; -export * from './extract-images-type.js'; -export * from './extract-pages-type.js'; -export * from './pdf-layers-type.js'; -export * from './sanitize-pdf-type.js'; -export * from './reverse-pages-type.js'; -export * from './text-color-type.js'; -export * from './n-up-pdf-type.js'; -export * from './linearize-pdf-type.js'; -export * from './remove-metadata-type.js'; -export * from './rotate-pdf-type.js'; -export * from './pdf-booklet-type.js'; -export * from './page-numbers-type.js'; -export * from './pdf-to-zip-type.js'; -export * from './sign-pdf-type.js'; -export * from './add-watermark-type.js'; - +export * from './ocr-type.ts'; +export * from './form-creator-type.ts'; +export * from './digital-sign-type.ts'; +export * from './attachment-type.ts'; +export * from './edit-attachments-type.ts'; +export * from './edit-metadata-type.ts'; +export * from './divide-pages-type.ts'; +export * from './alternate-merge-page-type.ts'; +export * from './add-blank-page-type.ts'; +export * from './compare-pdfs-type.ts'; +export * from './fix-page-size-type.ts'; +export * from './view-metadata-type.ts'; +export * from './header-footer-type.ts'; +export * from './encrypt-pdf-type.ts'; +export * from './flatten-pdf-type.ts'; +export * from './crop-pdf-type.ts'; +export * from './background-color-type.ts'; +export * from './posterize-type.ts'; +export * from './decrypt-pdf-type.ts'; +export * from './combine-single-page-type.ts'; +export * from './change-permissions-type.ts'; +export * from './validate-signature-type.ts'; +export * from './remove-restrictions-type.ts'; +export * from './page-dimensions-type.ts'; +export * from './extract-attachments-type.ts'; +export * from './pdf-multi-tool-type.ts'; +export * from './ocr-pdf-type.ts'; +export * from './delete-pages-type.ts'; +export * from './invert-colors-type.ts'; +export * from './table-of-contents-type.ts'; +export * from './organize-pdf-type.ts'; +export * from './merge-pdf-type.ts'; +export * from './extract-images-type.ts'; +export * from './extract-pages-type.ts'; +export * from './pdf-layers-type.ts'; +export * from './sanitize-pdf-type.ts'; +export * from './reverse-pages-type.ts'; +export * from './text-color-type.ts'; +export * from './n-up-pdf-type.ts'; +export * from './linearize-pdf-type.ts'; +export * from './remove-metadata-type.ts'; +export * from './rotate-pdf-type.ts'; +export * from './pdf-booklet-type.ts'; +export * from './page-numbers-type.ts'; +export * from './pdf-to-zip-type.ts'; +export * from './sign-pdf-type.ts'; +export * from './add-watermark-type.ts'; +export * from './email-to-pdf-type.ts'; diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index 25e9cd4b0..b760234a0 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -2,8 +2,7 @@ import createModule from '@neslinesli93/qpdf-wasm'; import { showLoader, hideLoader, showAlert } from '../ui.js'; import { createIcons } from 'lucide'; import { state, resetState } from '../state.js'; -import * as pdfjsLib from 'pdfjs-dist' - +import * as pdfjsLib from 'pdfjs-dist'; const STANDARD_SIZES = { A4: { width: 595.28, height: 841.89 }, @@ -50,14 +49,14 @@ export function convertPoints(points: any, unit: any) { // Convert hex color to RGB export function hexToRgb(hex: string): { r: number; g: number; b: number } { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255, - } - : { r: 0, g: 0, b: 0 } + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255, + } + : { r: 0, g: 0, b: 0 }; } export const formatBytes = (bytes: any, decimals = 1) => { @@ -89,7 +88,10 @@ export const readFileAsArrayBuffer = (file: any) => { }); }; -export function parsePageRanges(rangeString: string, totalPages: number): number[] { +export function parsePageRanges( + rangeString: string, + totalPages: number +): number[] { if (!rangeString || rangeString.trim() === '') { return Array.from({ length: totalPages }, (_, i) => i); } @@ -128,11 +130,9 @@ export function parsePageRanges(rangeString: string, totalPages: number): number } } - return Array.from(indices).sort((a, b) => a - b); } - /** * Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00") * into a localized, human-readable string. @@ -198,7 +198,7 @@ export function formatStars(num: number) { return (num / 1000).toFixed(1) + 'K'; } return num.toLocaleString(); -}; +} /** * Truncates a filename to a maximum length, adding ellipsis if needed. @@ -207,14 +207,18 @@ export function formatStars(num: number) { * @param maxLength - Maximum length (default: 30) * @returns Truncated filename with ellipsis if needed */ -export function truncateFilename(filename: string, maxLength: number = 25): string { +export function truncateFilename( + filename: string, + maxLength: number = 25 +): string { if (filename.length <= maxLength) { return filename; } const lastDotIndex = filename.lastIndexOf('.'); const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : ''; - const nameWithoutExt = lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename; + const nameWithoutExt = + lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename; const availableLength = maxLength - extension.length - 3; // 3 for '...' @@ -225,7 +229,10 @@ export function truncateFilename(filename: string, maxLength: number = 25): stri return nameWithoutExt.substring(0, availableLength) + '...' + extension; } -export function formatShortcutDisplay(shortcut: string, isMac: boolean): string { +export function formatShortcutDisplay( + shortcut: string, + isMac: boolean +): string { if (!shortcut) return ''; return shortcut .replace('mod', isMac ? '⌘' : 'Ctrl') @@ -233,7 +240,7 @@ export function formatShortcutDisplay(shortcut: string, isMac: boolean): string .replace('alt', isMac ? '⌥' : 'Alt') .replace('shift', 'Shift') .split('+') - .map(k => k.charAt(0).toUpperCase() + k.slice(1)) + .map((k) => k.charAt(0).toUpperCase() + k.slice(1)) .join(isMac ? '' : '+'); } @@ -263,7 +270,7 @@ export function resetAndReloadTool(preResetCallback?: () => void) { export function getPDFDocument(src: any) { let params = src; - // Handle different input types similar to how getDocument handles them, + // Handle different input types similar to how getDocument handles them, // but we ensure we have an object to attach wasmUrl to. if (typeof src === 'string') { params = { url: src }; @@ -283,3 +290,19 @@ export function getPDFDocument(src: any) { wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/', }); } + +/** + * Escape HTML special characters to prevent XSS + * @param text - The text to escape + * @returns The escaped text + */ +export function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replace(/[&<>"']/g, (m) => map[m]); +} diff --git a/src/pages/.backup-related-tools-fix/compress-pdf.html b/src/pages/.backup-related-tools-fix/compress-pdf.html deleted file mode 100644 index f69408643..000000000 --- a/src/pages/.backup-related-tools-fix/compress-pdf.html +++ /dev/null @@ -1,864 +0,0 @@ - - - - - - - - - Compress PDF Online Free - Reduce PDF Size Up to 90% | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -

- Compress PDF Free Online - Reduce File Size Fast -

-

- Reduce file size by choosing the compression method that best suits - your document. Supports multiple PDFs. -

- -
-
- -

- Click to select files - or drag and drop -

-

One or more PDF files

-

- Your files never leave your device. -

-
- -
- - - -
- - -
-
- - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload PDFs

-

- Click or drag and drop one or more PDF files -

-
-
-
-
- 2 -
-
-

- Choose Algorithm -

-

- Select Condense (recommended) or Photon (for photo-heavy PDFs) -

-
-
-
-
- 3 -
-
-

- Select Compression Level -

-

- Pick Light, Balanced, Aggressive, or Extreme compression -

-
-
-
-
- 4 -
-
-

- Customize & Compress -

-

- Optionally enable grayscale or custom settings, then compress -

-
-
-
-
- 5 -
-
-

Download

-

- Save your compressed PDFs - up to 90% smaller -

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - What's the difference between Condense and Photon? - - -

- Condense removes dead-weight, optimizes images, and subsets fonts - - best for most PDFs. Photon converts pages to images - ideal for - photo-heavy/scanned PDFs but makes text non-selectable. -

-
-
- - Which compression level should I use? - - -

- Balanced (recommended) offers great size reduction with minimal - quality loss. Use Light to preserve maximum quality, Aggressive for - smaller files, or Extreme for maximum compression. -

-
-
- - Are there file size limits? - - -

- No limits! Compress as many PDFs as you need, of any size, - completely free. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/edit-pdf.html b/src/pages/.backup-related-tools-fix/edit-pdf.html deleted file mode 100644 index 3dc991e79..000000000 --- a/src/pages/.backup-related-tools-fix/edit-pdf.html +++ /dev/null @@ -1,634 +0,0 @@ - - - - - - - - Edit PDF Online Free - Edit PDF Tool | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PDF Editor - BentoPDF - - - - - - - - - - - - - -
-
- - -

- Edit PDF Free Online - Modify PDFs Securely -

-

- Annotate, highlight, redact, comment, add shapes/images, search, and - view PDFs. -

- -
-
- -

- Click to select a file - or drag and drop -

-

PDF file

-

- Your files never leave your device. -

-
- -
- -
- - -
-
- - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload File

-

- Click or drag and drop your file to begin -

-
-
-
-
- 2 -
-
-

Process

-

Click the process button to start

-
-
-
-
- 3 -
-
-

Download

-

Save your processed file instantly

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Is edit pdf really free? - - -

- Yes! BentoPDF is 100% free with no hidden fees, no signup required, - and unlimited file processing. -

-
-
- - Are my files private and secure? - - -

- Absolutely! All processing happens in your browser. Your files never - leave your device, ensuring complete privacy. -

-
-
- - Is there a file size limit? - - -

- No! Process files of any size, as many times as you want, completely - free. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/merge-pdf.html b/src/pages/.backup-related-tools-fix/merge-pdf.html deleted file mode 100644 index a6a981cdd..000000000 --- a/src/pages/.backup-related-tools-fix/merge-pdf.html +++ /dev/null @@ -1,754 +0,0 @@ - - - - - - - - - Merge PDF Files Free Online - Combine Multiple PDFs | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -

- Merge PDF Files Free - Combine PDFs Instantly -

-

- Combine whole files, or select specific pages to merge into a new - document. -

- - -
-
- -

- Click to select a file - or drag and drop -

-

- PDFs or Images -

-

- Your files never leave your device. -

-
- -
- - - - - -
-
- - - - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload PDFs

-

- Select or drag and drop multiple PDF files you want to merge -

-
-
-
-
- 2 -
-
-

Arrange Order

-

- Drag files up or down to reorder them in your preferred sequence -

-
-
-
-
- 3 -
-
-

Merge Files

-

- Click the merge button to combine all PDFs into a single document -

-
-
-
-
- 4 -
-
-

Download

-

- Save your merged PDF - all pages combined in the order you chose -

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - How many PDFs can I merge at once? - - -

- Unlimited! Merge as many PDF files as you need in a single - operation. No restrictions on file count or total size. -

-
-
- - Will merging reduce PDF quality? - - -

- No! BentoPDF preserves the original quality of all PDFs when - merging. Your documents remain crisp and clear with no quality loss. -

-
-
- - Can I reorder pages after selecting files? - - -

- Yes! Simply drag and drop files to arrange them in any order before - clicking the merge button. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/organize-pdf.html b/src/pages/.backup-related-tools-fix/organize-pdf.html deleted file mode 100644 index a4ffdc78d..000000000 --- a/src/pages/.backup-related-tools-fix/organize-pdf.html +++ /dev/null @@ -1,576 +0,0 @@ - - - - - - - - Organize PDF Online Free - Organize PDF Tool | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Duplicate and Organize PDF Pages - BentoPDF - - - - - - - - - - - - - -
-
- -

- Organize PDF Pages Free - Reorder PDFs Online -

-

- Drag pages to reorder them. Use the - - icon to duplicate a page or the - - icon to delete it. -

- - -
-
- -

- Click to select a file - or drag and drop -

-

PDF Documents

-

- Your files never leave your device. -

-
- -
- - -
- - - - - -
-
- - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload File

-

- Click or drag and drop your file to begin -

-
-
-
-
- 2 -
-
-

Process

-

Click the process button to start

-
-
-
-
- 3 -
-
-

Download

-

Save your processed file instantly

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Is organize pdf really free? - - -

- Yes! BentoPDF is 100% free with no hidden fees, no signup required, - and unlimited file processing. -

-
-
- - Are my files private and secure? - - -

- Absolutely! All processing happens in your browser. Your files never - leave your device, ensuring complete privacy. -

-
-
- - Is there a file size limit? - - -

- No! Process files of any size, as many times as you want, completely - free. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/pdf-to-docx.html b/src/pages/.backup-related-tools-fix/pdf-to-docx.html deleted file mode 100644 index 78c49fd38..000000000 --- a/src/pages/.backup-related-tools-fix/pdf-to-docx.html +++ /dev/null @@ -1,638 +0,0 @@ - - - - - - - - PDF to Word Converter Free Online - Convert Files | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PDF to DOCX - BentoPDF - - - - - - - - - - - -
-
- - -

- PDF to Word Converter Free - Convert PDF to DOCX -

-

- Convert PDF files to editable Word documents. Preserves text, - formatting, and layout. -

- -
-
- -

- Click to select files - or drag and drop -

-

One or more PDF files

-

- Your files never leave your device. -

-
- -
- - - -
- - -
-
- - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload File

-

- Click or drag and drop your file to begin -

-
-
-
-
- 2 -
-
-

Process

-

Click the process button to start

-
-
-
-
- 3 -
-
-

Download

-

Save your processed file instantly

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Is pdf to docx really free? - - -

- Yes! BentoPDF is 100% free with no hidden fees, no signup required, - and unlimited file processing. -

-
-
- - Are my files private and secure? - - -

- Absolutely! All processing happens in your browser. Your files never - leave your device, ensuring complete privacy. -

-
-
- - Is there a file size limit? - - -

- No! Process files of any size, as many times as you want, completely - free. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/pdf-to-jpg.html b/src/pages/.backup-related-tools-fix/pdf-to-jpg.html deleted file mode 100644 index 227a2e9f4..000000000 --- a/src/pages/.backup-related-tools-fix/pdf-to-jpg.html +++ /dev/null @@ -1,677 +0,0 @@ - - - - - - - - PDF to JPG Converter Free Online - Convert Files | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PDF to JPG - BentoPDF - - - - - - - - - - - - - -
-
- - -

- PDF to JPG Converter Free - Convert PDF to Images -

-

- Convert each page of a PDF file into a high-quality JPG image. -

- -
-
- -

- Click to select a file - or drag and drop -

-

A single PDF file

-

- Your files never leave your device. -

-
- -
- -
- - -
-
- - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload PDF

-

- Select the PDF file you want to convert to images -

-
-
-
-
- 2 -
-
-

- Choose Quality -

-

- Select image quality (Low, Medium, High, Maximum) -

-
-
-
-
- 3 -
-
-

Select Pages

-

- Convert all pages or choose specific pages to extract -

-
-
-
-
- 4 -
-
-

- Download Images -

-

- Save JPG images individually or as a ZIP archive -

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - What quality should I choose? - - -

- Use High or Maximum for print-quality images, Medium for web use, or - Low to save disk space. Higher quality produces larger file sizes. -

-
-
- - Can I convert specific pages only? - - -

- Yes! You can choose to convert all pages or select specific pages - you want as JPG images. -

-
-
- - Are the JPG files compressed? - - -

- Yes, JPG is a compressed format. You control the quality level - - higher quality means less compression and larger files. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/rotate-pdf.html b/src/pages/.backup-related-tools-fix/rotate-pdf.html deleted file mode 100644 index 2e8c37073..000000000 --- a/src/pages/.backup-related-tools-fix/rotate-pdf.html +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - - - Rotate PDF Online Free - Rotate PDF Tool | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Rotate PDF - BentoPDF - - - - - - - - - - - - - - - -
-
- - -

- Rotate PDF Pages Free - Turn PDFs Online -

-

- Rotate individual pages or all pages at once. Click on page thumbnails - to rotate them. -

- -
-
- -

- Click to select a file - or drag and drop -

-

A single PDF file

-

- Your files never leave your device. -

-
- -
- -
- - -
-
- - - - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload File

-

- Click or drag and drop your file to begin -

-
-
-
-
- 2 -
-
-

Process

-

Click the process button to start

-
-
-
-
- 3 -
-
-

Download

-

Save your processed file instantly

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Is rotate pdf really free? - - -

- Yes! BentoPDF is 100% free with no hidden fees, no signup required, - and unlimited file processing. -

-
-
- - Are my files private and secure? - - -

- Absolutely! All processing happens in your browser. Your files never - leave your device, ensuring complete privacy. -

-
-
- - Is there a file size limit? - - -

- No! Process files of any size, as many times as you want, completely - free. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/split-pdf.html b/src/pages/.backup-related-tools-fix/split-pdf.html deleted file mode 100644 index 3561392a9..000000000 --- a/src/pages/.backup-related-tools-fix/split-pdf.html +++ /dev/null @@ -1,873 +0,0 @@ - - - - - - - - Split PDF Online Free - Split PDF Tool | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Split PDF - BentoPDF - - - - - - - - - - - - - -
-
- - -

- Split PDF Online Free - Extract Pages Easily -

-

- Extract pages from a PDF using various methods. -

- - -
-
- -

- Click to select a file - or drag and drop -

-

A single PDF file

-

- Your files never leave your device. -

-
- -
- -
- - -
-
- - - - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

Upload PDF

-

- Select the PDF file you want to split or extract pages from -

-
-
-
-
- 2 -
-
-

- Choose Split Method -

-

- Select specific pages, page ranges, or split into individual pages -

-
-
-
-
- 3 -
-
-

- Preview & Select -

-

- View page thumbnails and select exactly which pages you want -

-
-
-
-
- 4 -
-
-

- Split & Download -

-

- Download extracted pages as separate PDFs or as a ZIP file -

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Can I extract specific page ranges? - - -

- Yes! You can extract any specific pages or page ranges from your - PDF. Select exactly which pages you need or split into individual - files. -

-
-
- - Will split PDFs lose quality? - - -

- No! Split PDFs maintain the exact same quality as the original - document. No compression or quality loss occurs. -

-
-
- - Can I split multiple PDFs at once? - - -

- Yes, process multiple PDFs in batches for efficient document - management. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/word-to-pdf.html b/src/pages/.backup-related-tools-fix/word-to-pdf.html deleted file mode 100644 index ca8c1aa0f..000000000 --- a/src/pages/.backup-related-tools-fix/word-to-pdf.html +++ /dev/null @@ -1,678 +0,0 @@ - - - - - - - - Word to PDF Converter Free Online - Convert Files | BentoPDF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Word to PDF - Convert DOCX, DOC to PDF - BentoPDF - - - - - - - - - - - - - -
-
- - -

- Word to PDF Converter Free Online - Convert DOCX Fast -

-

- Convert Word documents (DOCX, DOC, ODT, RTF) to PDF format. Supports - multiple files. -

- -
-
- -

- Click to select files - or drag and drop -

-

- DOCX, DOC, ODT, RTF files -

-

- Your files never leave your device. -

-
- -
- -
- -

- First load takes a moment as we download our conversion engine. - After that, all loads will be instant. -

-
- - - -
- - -
-
- - - - - - -
-

- How It Works -

-
-
-
- 1 -
-
-

- Upload Word Files -

-

- Select or drag DOCX or DOC files you want to convert -

-
-
-
-
- 2 -
-
-

Auto Convert

-

- Conversion starts automatically, processing in your browser -

-
-
-
-
- 3 -
-
-

- Preview Results -

-

- See your converted PDF with preserved formatting -

-
-
-
-
- 4 -
-
-

Download PDFs

-

- Save your PDF files - formatting perfectly preserved -

-
-
-
-
- - -
-

- Related PDF Tools -

- -
- - -
-

- Frequently Asked Questions -

-
-
- - Does it work with both .doc and .docx files? - - -

- Yes! BentoPDF supports both older .doc and newer .docx Microsoft - Word formats. -

-
-
- - Will formatting be preserved? - - -

- Yes! Fonts, images, tables, headers, footers, and all formatting are - preserved exactly as they appear in Word. -

-
-
- - Can I convert password-protected Word files? - - -

- You'll need to unlock password-protected files first before - converting them to PDF. -

-
-
-
- - - - - - - - - - - - - - - - - diff --git a/src/pages/.backup-related-tools-fix/excel-to-pdf.html b/src/pages/email-to-pdf.html similarity index 74% rename from src/pages/.backup-related-tools-fix/excel-to-pdf.html rename to src/pages/email-to-pdf.html index 6ff772e98..71dc89ecc 100644 --- a/src/pages/.backup-related-tools-fix/excel-to-pdf.html +++ b/src/pages/email-to-pdf.html @@ -5,18 +5,20 @@ - Excel to PDF Converter Free Online - Convert Files | BentoPDF + + Email to PDF Converter Free Online - EML MSG to PDF | BentoPDF + - + - + @@ -48,35 +50,54 @@ - - + + - + - Excel to PDF - Convert XLSX, XLS to PDF - BentoPDF + Email to PDF - Convert EML/MSG to PDF - BentoPDF - - - - - + - + + + + + + + + + + @@ -185,13 +206,13 @@

- Excel to PDF Converter Free - Convert XLSX Online + Email to PDF Converter Free Online - Convert EML MSG Files

-

- Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. - Supports multiple files. +

+ Convert email files (EML, MSG) to PDF format. Supports Outlook exports + and standard email formats.

- XLSX, XLS, ODS, CSV files + EML, MSG files

Your files never leave your device. @@ -223,7 +244,7 @@ id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" - accept=".xls,.xlsx,.ods,.csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet,text/csv" + accept=".eml,.msg,message/rfc822,application/vnd.ms-outlook" multiple />

@@ -247,11 +268,63 @@
- @@ -324,7 +399,9 @@

Upload File

Process

-

Click the process button to start

+

+ Click the convert button to generate your PDF +

@@ -335,7 +412,7 @@

Process

Download

-

Save your processed file instantly

+

Save your converted PDF file instantly

@@ -348,39 +425,39 @@

@@ -395,7 +472,20 @@

- Is excel to pdf really free? + What email formats are supported? + + +

+ BentoPDF supports both .eml (standard email format) and .msg + (Microsoft Outlook) files. These are the most common formats for + exported or saved emails. +

+ +
+ + Is email to PDF conversion really free?

@@ -407,24 +497,26 @@

- Are my files private and secure? + Are my emails kept private?

- Absolutely! All processing happens in your browser. Your files never - leave your device, ensuring complete privacy. + Absolutely! All processing happens in your browser. Your email files + never leave your device, ensuring complete privacy for sensitive + communications.

- Is there a file size limit? + What about email attachments?

- No! Process files of any size, as many times as you want, completely - free. + The PDF displays a list of attachments at the bottom and also embeds + the actual files into the PDF. You can access them via the + Attachments panel in PDF readers like Adobe Reader or Foxit.

@@ -569,7 +661,7 @@

Follow Us

- + @@ -578,7 +670,7 @@

Follow Us

{ "@context": "https://schema.org", "@type": "SoftwareApplication", - "name": "Excel to PDF - BentoPDF", + "name": "Email to PDF - BentoPDF", "applicationCategory": "PDF Tool", "operatingSystem": "Any - Web Browser", "offers": { @@ -588,8 +680,8 @@

Follow Us

}, "aggregateRating": { "@type": "AggregateRating", - "ratingValue": "4.9", - "ratingCount": "1436" + "ratingValue": "4.7", + "ratingCount": "2156" } } @@ -598,26 +690,26 @@

Follow Us

{ "@context": "https://schema.org", "@type": "HowTo", - "name": "How to convert Excel to PDF online", - "description": "Learn how to convert Excel to PDF using BentoPDF", + "name": "How to convert email to PDF online", + "description": "Learn how to convert email files to PDF using BentoPDF", "step": [ { "@type": "HowToStep", "position": 1, - "name": "Upload File", - "text": "Click or drag and drop your file" + "name": "Upload Email File", + "text": "Click or drag and drop your .eml or .msg file" }, { "@type": "HowToStep", "position": 2, "name": "Process", - "text": "Click the process button" + "text": "Click the convert button" }, { "@type": "HowToStep", "position": 3, "name": "Download", - "text": "Download your processed file" + "text": "Download your converted PDF file" } ] } @@ -637,8 +729,8 @@

Follow Us

{ "@type": "ListItem", "position": 2, - "name": "Excel to PDF", - "item": "https://www.bentopdf.com/excel-to-pdf" + "name": "Email to PDF", + "item": "https://www.bentopdf.com/email-to-pdf" } ] } diff --git a/vite.config.ts b/vite.config.ts index eedfe5098..e3e4c424a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -418,6 +418,11 @@ export default defineConfig(({ mode }) => { __dirname, 'src/pages/digital-sign-pdf.html' ), + 'validate-signature-pdf': resolve( + __dirname, + 'src/pages/validate-signature-pdf.html' + ), + 'email-to-pdf': resolve(__dirname, 'src/pages/email-to-pdf.html'), }, }, }, From 1f7238d0b5b4e3b86bf5af89eb9909759886a7d4 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Fri, 9 Jan 2026 20:53:36 +0530 Subject: [PATCH 14/39] feat: add Deskew PDF and Font to Outline tools with improved issue templates New Features: - Add Deskew PDF tool for straightening scanned/skewed PDF pages - Add Font to Outline tool for converting text to vector paths - Add translations for new tools in all supported locales (de, en, id, it, tr, vi, zh) Improvements: - Migrate GitHub issue templates from markdown to YAML forms - Separate templates for bug reports, feature requests, and questions - Add config.yml for issue template chooser - Update sitemap.xml with new tool pages - Update ghostscript loader and helper utilities --- .../ISSUE_TEMPLATE/bug_feature_question.md | 80 -- .github/ISSUE_TEMPLATE/bug_report.yml | 122 +++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 39 + .github/ISSUE_TEMPLATE/question.yml | 30 + package-lock.json | 8 +- package.json | 2 +- public/locales/de/tools.json | 8 + public/locales/en/tools.json | 8 + public/locales/id/tools.json | 8 + public/locales/it/tools.json | 8 + public/locales/tr/tools.json | 8 + public/locales/vi/tools.json | 8 + public/locales/zh/tools.json | 8 + public/sitemap.xml | 12 + src/js/config/tools.ts | 13 + src/js/logic/compress-pdf-page.ts | 993 ++++++++++-------- src/js/logic/deskew-pdf-page.ts | 255 +++++ src/js/logic/email-to-pdf.ts | 105 +- src/js/logic/font-to-outline-page.ts | 222 ++++ src/js/main.ts | 2 + src/js/utils/ghostscript-loader.ts | 205 +++- src/js/utils/helpers.ts | 154 +++ src/pages/deskew-pdf.html | 677 ++++++++++++ src/pages/font-to-outline.html | 667 ++++++++++++ vite.config.ts | 5 + 26 files changed, 3002 insertions(+), 653 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_feature_question.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 src/js/logic/deskew-pdf-page.ts create mode 100644 src/js/logic/font-to-outline-page.ts create mode 100644 src/pages/deskew-pdf.html create mode 100644 src/pages/font-to-outline.html diff --git a/.github/ISSUE_TEMPLATE/bug_feature_question.md b/.github/ISSUE_TEMPLATE/bug_feature_question.md deleted file mode 100644 index 23d24e36d..000000000 --- a/.github/ISSUE_TEMPLATE/bug_feature_question.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: '🐛 Bug / 💡 Feature / ❓ Question' -about: 'Report a bug, request a feature, or ask a question about BentoPDF' -title: '(Bug) , (Feature) , or (Question) ' -labels: ['needs triage'] -assignees: [] ---- - -## Type of Issue - -Please check one: - -- [ ] 🐛 Bug Report -- [ ] 💡 Feature Request -- [ ] ❓ Question / Help - ---- - -## Description - -Provide a clear and concise description of the issue, feature request, or question. - ---- - -## Steps to Reproduce (for Bugs) - -1. Go to '...' -2. Run '...' -3. Observe error: '...' - -**Expected Behavior:** -Describe what you expected BentoPDF to do. - -**Actual Behavior:** -Describe what actually happened, including error messages. - ---- - -## Feature Request Details (if applicable) - -- What functionality are you requesting? -- Why is this useful? -- Any example or context to illustrate it? - ---- - -## Question Details (if applicable) - -- What is your question? -- What have you tried so far? -- Any relevant code snippet or scenario? - ---- - -## Screenshots / Logs (if applicable) - -Attach any screenshots, logs, or stack traces that help explain the problem or question. - ---- - -## Environment - -- **OS:** (e.g., macOS 14.0 / Ubuntu 22.04 / Windows 11) -- **Dependencies / setup details (if any):** - ---- - -## 💭 Additional Context - -Any other information, suggestions, or references that might help maintainers. - ---- - -✅ **Title Format Reminder:** - -- `(Bug) Text alignment incorrect on multi-line paragraphs` -- `(Feature) Add support for custom PDF metadata` -- `(Question) How to embed custom fonts?` - ---- diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..b638cfd6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,122 @@ +name: 🐛 Bug Report +description: Report a bug in BentoPDF +title: "(Bug) " +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + ## ⚠️ Important Notice + **Bug reports without logs or a sample file demonstrating the issue will not be investigated.** + Please help us help you by providing the information needed to reproduce and fix the problem. + + - type: textarea + id: description + attributes: + label: Description + description: Provide a clear and concise description of the bug. + placeholder: What happened? What did you expect to happen? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Upload file '...' + 4. See error + validations: + required: true + + - type: textarea + id: console-logs + attributes: + label: Console Logs + description: Open browser DevTools (F12 → Console tab) and paste any errors here. + placeholder: Paste console logs here... + render: shell + validations: + required: true + + - type: textarea + id: sample-file + attributes: + label: Sample PDF or File + description: | + Attach a sample PDF that reproduces the issue, or describe how to create one. + If you cannot share the original, create a minimal example that shows the problem. + placeholder: Drag and drop your file here, or describe how to reproduce with any PDF... + validations: + required: true + + - type: dropdown + id: browser + attributes: + label: Browser + description: Which browser are you using? + options: + - Chrome + - Firefox + - Safari + - Edge + - Brave + - Other + validations: + required: true + + - type: input + id: browser-version + attributes: + label: Browser Version + description: e.g., Chrome 120, Firefox 121 + placeholder: "120" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - macOS + - Windows + - Linux + - iOS + - Android + - Other + validations: + required: true + + - type: input + id: bentopdf-version + attributes: + label: BentoPDF Version + description: Check the footer or package.json + placeholder: "1.15.4" + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other information that might help us debug this issue. + placeholder: Screenshots, network errors, stack traces, etc. + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + options: + - label: I have included console logs from the browser DevTools + required: true + - label: I have attached a sample file or described how to reproduce the issue + required: true + - label: I have searched existing issues to ensure this is not a duplicate + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3c041abfb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 Discord Community + url: https://discord.gg/Bgq3Ay3f2w + about: Join our Discord for quick questions and community support + - name: 📖 Documentation + url: https://github.com/nicholaschen09/BentoPDF#readme + about: Check the README for setup and usage instructions diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..9797c6aa6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: 💡 Feature Request +description: Suggest a new feature for BentoPDF +title: "(Feature) " +labels: ["enhancement", "needs triage"] +body: + - type: textarea + id: description + attributes: + label: Feature Description + description: What functionality are you requesting? + placeholder: Describe the feature you'd like to see... + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Why is this feature useful? What problem does it solve? + placeholder: Explain why you need this feature... + validations: + required: true + + - type: textarea + id: examples + attributes: + label: Examples + description: Any examples, mockups, or references to illustrate the feature? + placeholder: Links to similar features, screenshots, etc. + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other information about the feature request. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 000000000..ac5e90507 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,30 @@ +name: ❓ Question +description: Ask a question about BentoPDF +title: "(Question) " +labels: ["question"] +body: + - type: textarea + id: question + attributes: + label: Question + description: What would you like to know? + placeholder: Your question here... + validations: + required: true + + - type: textarea + id: tried + attributes: + label: What have you tried? + description: What solutions have you already attempted? + placeholder: Describe what you've tried so far... + validations: + required: false + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any relevant code snippets, screenshots, or scenarios. + validations: + required: false diff --git a/package-lock.json b/package-lock.json index 849da8cff..df8e4b9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-only", "dependencies": { "@bentopdf/gs-wasm": "^0.1.0", - "@bentopdf/pymupdf-wasm": "^0.1.11", + "@bentopdf/pymupdf-wasm": "^0.11.12", "@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/dancing-script": "^5.2.8", "@fontsource/dm-sans": "^5.2.8", @@ -516,9 +516,9 @@ } }, "node_modules/@bentopdf/pymupdf-wasm": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@bentopdf/pymupdf-wasm/-/pymupdf-wasm-0.1.11.tgz", - "integrity": "sha512-sbDFmvm2KzT3oCmqNqMx7w6TMsKpLXeooVK8EVRjyQIV4hU5Ioq0JxWMr8SX7MESu8Caz1feeELd6zt5K966SA==", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/@bentopdf/pymupdf-wasm/-/pymupdf-wasm-0.11.12.tgz", + "integrity": "sha512-AcSg7v7pVhYcH23qLDEj3yTABlGIkZULPmrvWHRtEyD5QMS0TWOLUq/c0ATO371PKVlI4jEUpCBUj+iBsFJwVQ==", "license": "AGPL-3.0", "peerDependencies": { "@bentopdf/gs-wasm": "*" diff --git a/package.json b/package.json index a4d40e7bb..a29fcbea7 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "@bentopdf/gs-wasm": "^0.1.0", - "@bentopdf/pymupdf-wasm": "^0.1.11", + "@bentopdf/pymupdf-wasm": "^0.11.12", "@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/dancing-script": "^5.2.8", "@fontsource/dm-sans": "^5.2.8", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index d91af6e73..769e59194 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -521,5 +521,13 @@ "subtitle": "E-Mail-Dateien (EML, MSG) in PDF-Format konvertieren. Unterstützt Outlook-Exporte und Standard-E-Mail-Formate.", "acceptedFormats": "EML, MSG-Dateien", "convertButton": "In PDF konvertieren" + }, + "fontToOutline": { + "name": "Schriftart zu Umriss", + "subtitle": "Alle Schriftarten in Vektorumrisse für konsistente Darstellung auf allen Geräten konvertieren." + }, + "deskewPdf": { + "name": "PDF entzerren", + "subtitle": "Automatisch schiefe gescannte Seiten mit OpenCV begradigen." } } diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index a85839ca1..e09348062 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -521,5 +521,13 @@ "subtitle": "Convert email files (EML, MSG) to PDF format. Supports Outlook exports and standard email formats.", "acceptedFormats": "EML, MSG files", "convertButton": "Convert to PDF" + }, + "fontToOutline": { + "name": "Font to Outline", + "subtitle": "Convert all fonts to vector outlines for consistent rendering across all devices." + }, + "deskewPdf": { + "name": "Deskew PDF", + "subtitle": "Automatically straighten tilted scanned pages using OpenCV." } } diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index 497ccdcf3..95e9bb6bb 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -521,5 +521,13 @@ "subtitle": "Konversi file email (EML, MSG) ke format PDF. Mendukung ekspor Outlook dan format email standar.", "acceptedFormats": "File EML, MSG", "convertButton": "Konversi ke PDF" + }, + "fontToOutline": { + "name": "Font ke Garis Tepi", + "subtitle": "Konversi semua font ke garis tepi vektor untuk tampilan konsisten di semua perangkat." + }, + "deskewPdf": { + "name": "Luruskan PDF", + "subtitle": "Otomatis meluruskan halaman hasil pindai yang miring menggunakan OpenCV." } } diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 1a9960b91..9992060a7 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -494,5 +494,13 @@ "subtitle": "Converti file email (EML, MSG) in formato PDF. Supporta esportazioni Outlook e formati email standard.", "acceptedFormats": "File EML, MSG", "convertButton": "Converti in PDF" + }, + "fontToOutline": { + "name": "Font in Contorni", + "subtitle": "Converti tutti i font in contorni vettoriali per una visualizzazione coerente su tutti i dispositivi." + }, + "deskewPdf": { + "name": "Raddrizza PDF", + "subtitle": "Raddrizza automaticamente le pagine scansionate inclinate usando OpenCV." } } diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 8467d2559..0947fd319 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -284,5 +284,13 @@ "subtitle": "E-posta dosyalarını (EML, MSG) PDF formatına dönüştürün. Outlook dışa aktarmalarını ve standart e-posta formatlarını destekler.", "acceptedFormats": "EML, MSG Dosyaları", "convertButton": "PDF'ye Dönüştür" + }, + "fontToOutline": { + "name": "Yazı Tipi Çerçeveye Dönüştür", + "subtitle": "Tüm yazı tiplerini vektör çerçevelere dönüştürün, tüm cihazlarda tutarlı görüntü için." + }, + "deskewPdf": { + "name": "PDF Eğriliğini Düzelt", + "subtitle": "OpenCV kullanarak eğik taranmış sayfaları otomatik olarak düzeltin." } } diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index 21df99136..04b37af23 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -521,5 +521,13 @@ "subtitle": "Chuyển đổi tệp email (EML, MSG) sang định dạng PDF. Hỗ trợ xuất Outlook và định dạng email tiêu chuẩn.", "acceptedFormats": "Tệp EML, MSG", "convertButton": "Chuyển đổi sang PDF" + }, + "fontToOutline": { + "name": "Phông chữ thành đường viền", + "subtitle": "Chuyển đổi tất cả phông chữ thành đường viền vector để hiển thị nhất quán trên mọi thiết bị." + }, + "deskewPdf": { + "name": "Chỉnh nghiêng PDF", + "subtitle": "Tự động làm thẳng các trang quét bị nghiêng bằng OpenCV." } } diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index fca82d65d..a93c82213 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -518,5 +518,13 @@ "subtitle": "将电子邮件文件 (EML, MSG) 转换为 PDF 格式。支持 Outlook 导出和标准邮件格式。", "acceptedFormats": "EML, MSG 文件", "convertButton": "转换为 PDF" + }, + "fontToOutline": { + "name": "字体转轮廓", + "subtitle": "将所有字体转换为矢量轮廓,确保在所有设备上一致呈现。" + }, + "deskewPdf": { + "name": "校正 PDF", + "subtitle": "使用 OpenCV 自动校正倾斜的扫描页面。" } } diff --git a/public/sitemap.xml b/public/sitemap.xml index 7613a80aa..12fa5dcc2 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -699,6 +699,18 @@ monthly 0.5
+ + https://www.bentopdf.com/deskew-pdf + 2026-01-08 + monthly + 0.5 + + + https://www.bentopdf.com/font-to-outline + 2026-01-08 + monthly + 0.5 + diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 9720e186b..42990902f 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -693,6 +693,19 @@ export const categories = [ subtitle: 'Convert PDF to image-based PDF. Flatten layers and remove selectable text.', }, + { + href: import.meta.env.BASE_URL + 'deskew-pdf.html', + name: 'Deskew PDF', + icon: 'ph-perspective', + subtitle: 'Automatically straighten tilted scanned pages using OpenCV.', + }, + { + href: import.meta.env.BASE_URL + 'font-to-outline.html', + name: 'Font to Outline', + icon: 'ph-text-outdent', + subtitle: + 'Convert all fonts to vector outlines for consistent rendering.', + }, ], }, { diff --git a/src/js/logic/compress-pdf-page.ts b/src/js/logic/compress-pdf-page.ts index 8aea67299..6777f368a 100644 --- a/src/js/logic/compress-pdf-page.ts +++ b/src/js/logic/compress-pdf-page.ts @@ -1,9 +1,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, - getPDFDocument, + downloadFile, + readFileAsArrayBuffer, + formatBytes, + getPDFDocument, } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; @@ -12,485 +12,580 @@ import { PyMuPDF } from '@bentopdf/pymupdf-wasm'; import { getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import * as pdfjsLib from 'pdfjs-dist'; -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); const CONDENSE_PRESETS = { - light: { - images: { quality: 90, dpiTarget: 150, dpiThreshold: 200 }, - scrub: { metadata: false, thumbnails: true }, - subsetFonts: true, - }, - balanced: { - images: { quality: 75, dpiTarget: 96, dpiThreshold: 150 }, - scrub: { metadata: true, thumbnails: true }, - subsetFonts: true, - }, - aggressive: { - images: { quality: 50, dpiTarget: 72, dpiThreshold: 100 }, - scrub: { metadata: true, thumbnails: true, xmlMetadata: true }, - subsetFonts: true, - }, - extreme: { - images: { quality: 30, dpiTarget: 60, dpiThreshold: 96 }, - scrub: { metadata: true, thumbnails: true, xmlMetadata: true }, - subsetFonts: true, - }, + light: { + images: { quality: 90, dpiTarget: 150, dpiThreshold: 200 }, + scrub: { metadata: false, thumbnails: true }, + subsetFonts: true, + }, + balanced: { + images: { quality: 75, dpiTarget: 96, dpiThreshold: 150 }, + scrub: { metadata: true, thumbnails: true }, + subsetFonts: true, + }, + aggressive: { + images: { quality: 50, dpiTarget: 72, dpiThreshold: 100 }, + scrub: { metadata: true, thumbnails: true, xmlMetadata: true }, + subsetFonts: true, + }, + extreme: { + images: { quality: 30, dpiTarget: 60, dpiThreshold: 96 }, + scrub: { metadata: true, thumbnails: true, xmlMetadata: true }, + subsetFonts: true, + }, }; const PHOTON_PRESETS = { - light: { scale: 2.0, quality: 0.85 }, - balanced: { scale: 1.5, quality: 0.65 }, - aggressive: { scale: 1.2, quality: 0.45 }, - extreme: { scale: 1.0, quality: 0.25 }, + light: { scale: 2.0, quality: 0.85 }, + balanced: { scale: 1.5, quality: 0.65 }, + aggressive: { scale: 1.2, quality: 0.45 }, + extreme: { scale: 1.0, quality: 0.25 }, }; async function performCondenseCompression( - fileBlob: Blob, - level: string, - customSettings?: { - imageQuality?: number; - dpiTarget?: number; - dpiThreshold?: number; - removeMetadata?: boolean; - subsetFonts?: boolean; - convertToGrayscale?: boolean; - removeThumbnails?: boolean; - } + fileBlob: Blob, + level: string, + customSettings?: { + imageQuality?: number; + dpiTarget?: number; + dpiThreshold?: number; + removeMetadata?: boolean; + subsetFonts?: boolean; + convertToGrayscale?: boolean; + removeThumbnails?: boolean; + } ) { - const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf')); - await pymupdf.load(); - - const preset = CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] || CONDENSE_PRESETS.balanced; - - const dpiTarget = customSettings?.dpiTarget ?? preset.images.dpiTarget; - const userThreshold = customSettings?.dpiThreshold ?? preset.images.dpiThreshold; - const dpiThreshold = Math.max(userThreshold, dpiTarget + 10); - - const options = { + const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf')); + await pymupdf.load(); + + const preset = + CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] || + CONDENSE_PRESETS.balanced; + + const dpiTarget = customSettings?.dpiTarget ?? preset.images.dpiTarget; + const userThreshold = + customSettings?.dpiThreshold ?? preset.images.dpiThreshold; + const dpiThreshold = Math.max(userThreshold, dpiTarget + 10); + + const options = { + images: { + enabled: true, + quality: customSettings?.imageQuality ?? preset.images.quality, + dpiTarget, + dpiThreshold, + convertToGray: customSettings?.convertToGrayscale ?? false, + }, + scrub: { + metadata: customSettings?.removeMetadata ?? preset.scrub.metadata, + thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails, + xmlMetadata: (preset.scrub as any).xmlMetadata ?? false, + }, + subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts, + save: { + garbage: 4 as const, + deflate: true, + clean: true, + useObjstms: true, + }, + }; + + try { + const result = await pymupdf.compressPdf(fileBlob, options); + return result; + } catch (error: any) { + const errorMessage = error?.message || String(error); + if ( + errorMessage.includes('PatternType') || + errorMessage.includes('pattern') + ) { + console.warn( + '[CompressPDF] Pattern error detected, retrying without image rewriting:', + errorMessage + ); + + const fallbackOptions = { + ...options, images: { - enabled: true, - quality: customSettings?.imageQuality ?? preset.images.quality, - dpiTarget, - dpiThreshold, - convertToGray: customSettings?.convertToGrayscale ?? false, - }, - scrub: { - metadata: customSettings?.removeMetadata ?? preset.scrub.metadata, - thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails, - xmlMetadata: (preset.scrub as any).xmlMetadata ?? false, - }, - subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts, - save: { - garbage: 4 as const, - deflate: true, - clean: true, - useObjstms: true, + ...options.images, + enabled: false, }, - }; + }; - try { - const result = await pymupdf.compressPdf(fileBlob, options); - return result; - } catch (error: any) { - const errorMessage = error?.message || String(error); - if (errorMessage.includes('PatternType') || errorMessage.includes('pattern')) { - console.warn('[CompressPDF] Pattern error detected, retrying without image rewriting:', errorMessage); - - const fallbackOptions = { - ...options, - images: { - ...options.images, - enabled: false, - }, - }; - - const result = await pymupdf.compressPdf(fileBlob, fallbackOptions); - return { ...result, usedFallback: true }; - } - - throw new Error(`PDF compression failed: ${errorMessage}`); + const result = await pymupdf.compressPdf(fileBlob, fallbackOptions); + return { ...result, usedFallback: true }; } -} - -async function performPhotonCompression(arrayBuffer: ArrayBuffer, level: string) { - const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; - const newPdfDoc = await PDFDocument.create(); - const settings = PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || PHOTON_PRESETS.balanced; - - for (let i = 1; i <= pdfJsDoc.numPages; i++) { - const page = await pdfJsDoc.getPage(i); - const viewport = page.getViewport({ scale: settings.scale }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ canvasContext: context, viewport, canvas: canvas }).promise; + throw new Error(`PDF compression failed: ${errorMessage}`); + } +} - const jpegBlob = await new Promise((resolve) => - canvas.toBlob((blob) => resolve(blob as Blob), 'image/jpeg', settings.quality) - ); - const jpegBytes = await jpegBlob.arrayBuffer(); - const jpegImage = await newPdfDoc.embedJpg(jpegBytes); - const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); - newPage.drawImage(jpegImage, { - x: 0, - y: 0, - width: viewport.width, - height: viewport.height, - }); - } - return await newPdfDoc.save(); +async function performPhotonCompression( + arrayBuffer: ArrayBuffer, + level: string +) { + const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; + const newPdfDoc = await PDFDocument.create(); + const settings = + PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || + PHOTON_PRESETS.balanced; + + for (let i = 1; i <= pdfJsDoc.numPages; i++) { + const page = await pdfJsDoc.getPage(i); + const viewport = page.getViewport({ scale: settings.scale }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ canvasContext: context, viewport, canvas: canvas }) + .promise; + + const jpegBlob = await new Promise((resolve) => + canvas.toBlob( + (blob) => resolve(blob as Blob), + 'image/jpeg', + settings.quality + ) + ); + const jpegBytes = await jpegBlob.arrayBuffer(); + const jpegImage = await newPdfDoc.embedJpg(jpegBytes); + const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); + newPage.drawImage(jpegImage, { + x: 0, + y: 0, + width: viewport.width, + height: viewport.height, + }); + } + return await newPdfDoc.save(); } document.addEventListener('DOMContentLoaded', () => { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const compressOptions = document.getElementById('compress-options'); - const addMoreBtn = document.getElementById('add-more-btn'); - const clearFilesBtn = document.getElementById('clear-files-btn'); - const processBtn = document.getElementById('process-btn'); - const backBtn = document.getElementById('back-to-tools'); - const algorithmSelect = document.getElementById('compression-algorithm') as HTMLSelectElement; - const condenseInfo = document.getElementById('condense-info'); - const photonInfo = document.getElementById('photon-info'); - const toggleCustomSettings = document.getElementById('toggle-custom-settings'); - const customSettingsPanel = document.getElementById('custom-settings-panel'); - const customSettingsChevron = document.getElementById('custom-settings-chevron'); - - let useCustomSettings = false; - - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const compressOptions = document.getElementById('compress-options'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const processBtn = document.getElementById('process-btn'); + const backBtn = document.getElementById('back-to-tools'); + const algorithmSelect = document.getElementById( + 'compression-algorithm' + ) as HTMLSelectElement; + const condenseInfo = document.getElementById('condense-info'); + const photonInfo = document.getElementById('photon-info'); + const toggleCustomSettings = document.getElementById( + 'toggle-custom-settings' + ); + const customSettingsPanel = document.getElementById('custom-settings-panel'); + const customSettingsChevron = document.getElementById( + 'custom-settings-chevron' + ); + + let useCustomSettings = false; + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } + + // Toggle algorithm info + if (algorithmSelect && condenseInfo && photonInfo) { + algorithmSelect.addEventListener('change', () => { + if (algorithmSelect.value === 'condense') { + condenseInfo.classList.remove('hidden'); + photonInfo.classList.add('hidden'); + } else { + condenseInfo.classList.add('hidden'); + photonInfo.classList.remove('hidden'); + } + }); + } + + // Toggle custom settings panel + if (toggleCustomSettings && customSettingsPanel && customSettingsChevron) { + toggleCustomSettings.addEventListener('click', () => { + customSettingsPanel.classList.toggle('hidden'); + customSettingsChevron.style.transform = + customSettingsPanel.classList.contains('hidden') + ? 'rotate(0deg)' + : 'rotate(180deg)'; + // Mark that user wants to use custom settings + if (!customSettingsPanel.classList.contains('hidden')) { + useCustomSettings = true; + } + }); + } + + const updateUI = async () => { + if (!compressOptions) return; + + if (state.files.length > 0) { + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSpan = document.createElement('div'); + nameSpan.className = + 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); + + infoContainer.append(nameSpan, metaSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); + updateUI(); + }; - // Toggle algorithm info - if (algorithmSelect && condenseInfo && photonInfo) { - algorithmSelect.addEventListener('change', () => { - if (algorithmSelect.value === 'condense') { - condenseInfo.classList.remove('hidden'); - photonInfo.classList.add('hidden'); - } else { - condenseInfo.classList.add('hidden'); - photonInfo.classList.remove('hidden'); - } - }); - } + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + } - // Toggle custom settings panel - if (toggleCustomSettings && customSettingsPanel && customSettingsChevron) { - toggleCustomSettings.addEventListener('click', () => { - customSettingsPanel.classList.toggle('hidden'); - customSettingsChevron.style.transform = customSettingsPanel.classList.contains('hidden') - ? 'rotate(0deg)' - : 'rotate(180deg)'; - // Mark that user wants to use custom settings - if (!customSettingsPanel.classList.contains('hidden')) { - useCustomSettings = true; - } - }); + createIcons({ icons }); + } + compressOptions.classList.remove('hidden'); + } else { + compressOptions.classList.add('hidden'); + // Clear file display area + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + } + }; + + const resetState = () => { + state.files = []; + state.pdfDoc = null; + + const compressionLevel = document.getElementById( + 'compression-level' + ) as HTMLSelectElement; + if (compressionLevel) compressionLevel.value = 'balanced'; + + if (algorithmSelect) algorithmSelect.value = 'condense'; + + useCustomSettings = false; + if (customSettingsPanel) customSettingsPanel.classList.add('hidden'); + if (customSettingsChevron) + customSettingsChevron.style.transform = 'rotate(0deg)'; + + const imageQuality = document.getElementById( + 'image-quality' + ) as HTMLInputElement; + const dpiTarget = document.getElementById('dpi-target') as HTMLInputElement; + const dpiThreshold = document.getElementById( + 'dpi-threshold' + ) as HTMLInputElement; + const removeMetadata = document.getElementById( + 'remove-metadata' + ) as HTMLInputElement; + const subsetFonts = document.getElementById( + 'subset-fonts' + ) as HTMLInputElement; + const convertToGrayscale = document.getElementById( + 'convert-to-grayscale' + ) as HTMLInputElement; + const removeThumbnails = document.getElementById( + 'remove-thumbnails' + ) as HTMLInputElement; + + if (imageQuality) imageQuality.value = '75'; + if (dpiTarget) dpiTarget.value = '96'; + if (dpiThreshold) dpiThreshold.value = '150'; + if (removeMetadata) removeMetadata.checked = true; + if (subsetFonts) subsetFonts.checked = true; + if (convertToGrayscale) convertToGrayscale.checked = false; + if (removeThumbnails) removeThumbnails.checked = true; + + if (condenseInfo) condenseInfo.classList.remove('hidden'); + if (photonInfo) photonInfo.classList.add('hidden'); + + updateUI(); + }; + + const compress = async () => { + const level = ( + document.getElementById('compression-level') as HTMLSelectElement + ).value; + const algorithm = ( + document.getElementById('compression-algorithm') as HTMLSelectElement + ).value; + const convertToGrayscale = + (document.getElementById('convert-to-grayscale') as HTMLInputElement) + ?.checked ?? false; + + let customSettings: + | { + imageQuality?: number; + dpiTarget?: number; + dpiThreshold?: number; + removeMetadata?: boolean; + subsetFonts?: boolean; + convertToGrayscale?: boolean; + removeThumbnails?: boolean; + } + | undefined; + + if (useCustomSettings) { + const imageQuality = + parseInt( + (document.getElementById('image-quality') as HTMLInputElement)?.value + ) || 75; + const dpiTarget = + parseInt( + (document.getElementById('dpi-target') as HTMLInputElement)?.value + ) || 96; + const dpiThreshold = + parseInt( + (document.getElementById('dpi-threshold') as HTMLInputElement)?.value + ) || 150; + const removeMetadata = + (document.getElementById('remove-metadata') as HTMLInputElement) + ?.checked ?? true; + const subsetFonts = + (document.getElementById('subset-fonts') as HTMLInputElement) + ?.checked ?? true; + const removeThumbnails = + (document.getElementById('remove-thumbnails') as HTMLInputElement) + ?.checked ?? true; + + customSettings = { + imageQuality, + dpiTarget, + dpiThreshold, + removeMetadata, + subsetFonts, + convertToGrayscale, + removeThumbnails, + }; + } else { + customSettings = convertToGrayscale ? { convertToGrayscale } : undefined; } - const updateUI = async () => { - if (!compressOptions) return; - - if (state.files.length > 0) { - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; - - for (let index = 0; index < state.files.length; index++) { - const file = state.files[index]; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; - - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = file.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = formatBytes(file.size); + try { + if (state.files.length === 0) { + showAlert('No Files', 'Please select at least one PDF file.'); + hideLoader(); + return; + } + + if (state.files.length === 1) { + const originalFile = state.files[0]; + + let resultBlob: Blob; + let resultSize: number; + let usedMethod: string; + + if (algorithm === 'condense') { + showLoader('Running Condense compression...'); + const result = await performCondenseCompression( + originalFile, + level, + customSettings + ); + resultBlob = result.blob; + resultSize = result.compressedSize; + usedMethod = 'Condense'; + + // Check if fallback was used + if ((result as any).usedFallback) { + usedMethod += + ' (without image optimization due to unsupported patterns)'; + } + } else { + showLoader('Running Photon compression...'); + const arrayBuffer = (await readFileAsArrayBuffer( + originalFile + )) as ArrayBuffer; + const resultBytes = await performPhotonCompression( + arrayBuffer, + level + ); + const buffer = resultBytes.buffer.slice( + resultBytes.byteOffset, + resultBytes.byteOffset + resultBytes.byteLength + ) as ArrayBuffer; + resultBlob = new Blob([buffer], { type: 'application/pdf' }); + resultSize = resultBytes.length; + usedMethod = 'Photon'; + } - infoContainer.append(nameSpan, metaSpan); + const originalSize = formatBytes(originalFile.size); + const compressedSize = formatBytes(resultSize); + const savings = originalFile.size - resultSize; + const savingsPercent = + savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = () => { - state.files = state.files.filter((_, i) => i !== index); - updateUI(); - }; + downloadFile( + resultBlob, + originalFile.name.replace(/\.pdf$/i, '') + '_compressed.pdf' + ); - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - } + hideLoader(); - createIcons({ icons }); - } - compressOptions.classList.remove('hidden'); + if (savings > 0) { + showAlert( + 'Compression Complete', + `Method: ${usedMethod}. File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`, + 'success', + () => resetState() + ); } else { - compressOptions.classList.add('hidden'); - // Clear file display area - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + showAlert( + 'Compression Finished', + `Method: ${usedMethod}. Could not reduce file size further. Original: ${originalSize}, New: ${compressedSize}.`, + 'warning', + () => resetState() + ); } - }; - - const resetState = () => { - state.files = []; - state.pdfDoc = null; - - const compressionLevel = document.getElementById('compression-level') as HTMLSelectElement; - if (compressionLevel) compressionLevel.value = 'balanced'; - - if (algorithmSelect) algorithmSelect.value = 'condense'; - - useCustomSettings = false; - if (customSettingsPanel) customSettingsPanel.classList.add('hidden'); - if (customSettingsChevron) customSettingsChevron.style.transform = 'rotate(0deg)'; - - const imageQuality = document.getElementById('image-quality') as HTMLInputElement; - const dpiTarget = document.getElementById('dpi-target') as HTMLInputElement; - const dpiThreshold = document.getElementById('dpi-threshold') as HTMLInputElement; - const removeMetadata = document.getElementById('remove-metadata') as HTMLInputElement; - const subsetFonts = document.getElementById('subset-fonts') as HTMLInputElement; - const convertToGrayscale = document.getElementById('convert-to-grayscale') as HTMLInputElement; - const removeThumbnails = document.getElementById('remove-thumbnails') as HTMLInputElement; - - if (imageQuality) imageQuality.value = '75'; - if (dpiTarget) dpiTarget.value = '96'; - if (dpiThreshold) dpiThreshold.value = '150'; - if (removeMetadata) removeMetadata.checked = true; - if (subsetFonts) subsetFonts.checked = true; - if (convertToGrayscale) convertToGrayscale.checked = false; - if (removeThumbnails) removeThumbnails.checked = true; - - if (condenseInfo) condenseInfo.classList.remove('hidden'); - if (photonInfo) photonInfo.classList.add('hidden'); - - updateUI(); - }; - - const compress = async () => { - const level = (document.getElementById('compression-level') as HTMLSelectElement).value; - const algorithm = (document.getElementById('compression-algorithm') as HTMLSelectElement).value; - const convertToGrayscale = (document.getElementById('convert-to-grayscale') as HTMLInputElement)?.checked ?? false; - - let customSettings: { - imageQuality?: number; - dpiTarget?: number; - dpiThreshold?: number; - removeMetadata?: boolean; - subsetFonts?: boolean; - convertToGrayscale?: boolean; - removeThumbnails?: boolean; - } | undefined; - - if (useCustomSettings) { - const imageQuality = parseInt((document.getElementById('image-quality') as HTMLInputElement)?.value) || 75; - const dpiTarget = parseInt((document.getElementById('dpi-target') as HTMLInputElement)?.value) || 96; - const dpiThreshold = parseInt((document.getElementById('dpi-threshold') as HTMLInputElement)?.value) || 150; - const removeMetadata = (document.getElementById('remove-metadata') as HTMLInputElement)?.checked ?? true; - const subsetFonts = (document.getElementById('subset-fonts') as HTMLInputElement)?.checked ?? true; - const removeThumbnails = (document.getElementById('remove-thumbnails') as HTMLInputElement)?.checked ?? true; - - customSettings = { - imageQuality, - dpiTarget, - dpiThreshold, - removeMetadata, - subsetFonts, - convertToGrayscale, - removeThumbnails, - }; - } else { - customSettings = convertToGrayscale ? { convertToGrayscale } : undefined; - } - - try { - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one PDF file.'); - hideLoader(); - return; - } - - if (state.files.length === 1) { - const originalFile = state.files[0]; - - let resultBlob: Blob; - let resultSize: number; - let usedMethod: string; - - if (algorithm === 'condense') { - showLoader('Loading engine...'); - const result = await performCondenseCompression(originalFile, level, customSettings); - resultBlob = result.blob; - resultSize = result.compressedSize; - usedMethod = 'Condense'; - - // Check if fallback was used - if ((result as any).usedFallback) { - usedMethod += ' (without image optimization due to unsupported patterns)'; - } - } else { - showLoader('Running Photon compression...'); - const arrayBuffer = await readFileAsArrayBuffer(originalFile) as ArrayBuffer; - const resultBytes = await performPhotonCompression(arrayBuffer, level); - const buffer = resultBytes.buffer.slice(resultBytes.byteOffset, resultBytes.byteOffset + resultBytes.byteLength) as ArrayBuffer; - resultBlob = new Blob([buffer], { type: 'application/pdf' }); - resultSize = resultBytes.length; - usedMethod = 'Photon'; - } - - const originalSize = formatBytes(originalFile.size); - const compressedSize = formatBytes(resultSize); - const savings = originalFile.size - resultSize; - const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; - - downloadFile( - resultBlob, - originalFile.name.replace(/\.pdf$/i, '') + '_compressed.pdf' - ); - - hideLoader(); - - if (savings > 0) { - showAlert( - 'Compression Complete', - `Method: ${usedMethod}. File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`, - 'success', - () => resetState() - ); - } else { - showAlert( - 'Compression Finished', - `Method: ${usedMethod}. Could not reduce file size further. Original: ${originalSize}, New: ${compressedSize}.`, - 'warning', - () => resetState() - ); - } - } else { - showLoader('Compressing multiple PDFs...'); - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - let totalOriginalSize = 0; - let totalCompressedSize = 0; - - for (let i = 0; i < state.files.length; i++) { - const file = state.files[i]; - showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`); - totalOriginalSize += file.size; - - let resultBytes: Uint8Array; - if (algorithm === 'condense') { - const result = await performCondenseCompression(file, level, customSettings); - resultBytes = new Uint8Array(await result.blob.arrayBuffer()); - } else { - const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer; - resultBytes = await performPhotonCompression(arrayBuffer, level); - } - - totalCompressedSize += resultBytes.length; - const baseName = file.name.replace(/\.pdf$/i, ''); - zip.file(`${baseName}_compressed.pdf`, resultBytes); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - const totalSavings = totalOriginalSize - totalCompressedSize; - const totalSavingsPercent = totalSavings > 0 - ? ((totalSavings / totalOriginalSize) * 100).toFixed(1) - : 0; - - downloadFile(zipBlob, 'compressed-pdfs.zip'); - - hideLoader(); - - if (totalSavings > 0) { - showAlert( - 'Compression Complete', - `Compressed ${state.files.length} PDF(s). Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`, - 'success', - () => resetState() - ); - } else { - showAlert( - 'Compression Finished', - `Compressed ${state.files.length} PDF(s). Total size: ${formatBytes(totalCompressedSize)}.`, - 'info', - () => resetState() - ); - } - } - } catch (e: any) { - hideLoader(); - console.error('[CompressPDF] Error:', e); - showAlert( - 'Error', - `An error occurred during compression. Error: ${e.message}` + } else { + showLoader('Compressing multiple PDFs...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + let totalOriginalSize = 0; + let totalCompressedSize = 0; + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + showLoader( + `Compressing ${i + 1}/${state.files.length}: ${file.name}...` + ); + totalOriginalSize += file.size; + + let resultBytes: Uint8Array; + if (algorithm === 'condense') { + const result = await performCondenseCompression( + file, + level, + customSettings ); + resultBytes = new Uint8Array(await result.blob.arrayBuffer()); + } else { + const arrayBuffer = (await readFileAsArrayBuffer( + file + )) as ArrayBuffer; + resultBytes = await performPhotonCompression(arrayBuffer, level); + } + + totalCompressedSize += resultBytes.length; + const baseName = file.name.replace(/\.pdf$/i, ''); + zip.file(`${baseName}_compressed.pdf`, resultBytes); } - }; - const handleFileSelect = (files: FileList | null) => { - if (files && files.length > 0) { - state.files = [...state.files, ...Array.from(files)]; - updateUI(); - } - }; - - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf'); - if (pdfFiles.length > 0) { - const dataTransfer = new DataTransfer(); - pdfFiles.forEach(f => dataTransfer.items.add(f)); - handleFileSelect(dataTransfer.files); - } - } - }); - - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); - } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + const totalSavings = totalOriginalSize - totalCompressedSize; + const totalSavingsPercent = + totalSavings > 0 + ? ((totalSavings / totalOriginalSize) * 100).toFixed(1) + : 0; - if (addMoreBtn) { - addMoreBtn.addEventListener('click', () => { - fileInput.click(); - }); - } + downloadFile(zipBlob, 'compressed-pdfs.zip'); + + hideLoader(); - if (clearFilesBtn) { - clearFilesBtn.addEventListener('click', () => { - resetState(); - }); + if (totalSavings > 0) { + showAlert( + 'Compression Complete', + `Compressed ${state.files.length} PDF(s). Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`, + 'success', + () => resetState() + ); + } else { + showAlert( + 'Compression Finished', + `Compressed ${state.files.length} PDF(s). Total size: ${formatBytes(totalCompressedSize)}.`, + 'info', + () => resetState() + ); + } + } + } catch (e: any) { + hideLoader(); + console.error('[CompressPDF] Error:', e); + showAlert( + 'Error', + `An error occurred during compression. Error: ${e.message}` + ); } + }; - if (processBtn) { - processBtn.addEventListener('click', compress); + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const pdfFiles = Array.from(files).filter( + (f) => f.type === 'application/pdf' + ); + if (pdfFiles.length > 0) { + const dataTransfer = new DataTransfer(); + pdfFiles.forEach((f) => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); + + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', compress); + } }); diff --git a/src/js/logic/deskew-pdf-page.ts b/src/js/logic/deskew-pdf-page.ts new file mode 100644 index 000000000..e86a67e92 --- /dev/null +++ b/src/js/logic/deskew-pdf-page.ts @@ -0,0 +1,255 @@ +import { PyMuPDF } from '@bentopdf/pymupdf-wasm'; +import { createIcons, icons } from 'lucide'; +import { downloadFile } from '../utils/helpers'; + +interface DeskewResult { + totalPages: number; + correctedPages: number; + angles: number[]; + corrected: boolean[]; +} + +let selectedFiles: File[] = []; +let pymupdf: PyMuPDF | null = null; + +function initPyMuPDF(): PyMuPDF { + if (!pymupdf) { + pymupdf = new PyMuPDF({ + assetPath: import.meta.env.BASE_URL + 'pymupdf-wasm/', + }); + } + return pymupdf; +} + +function showLoader(message: string): void { + const loader = document.getElementById('loader-modal'); + const text = document.getElementById('loader-text'); + if (loader && text) { + text.textContent = message; + loader.classList.remove('hidden'); + } +} + +function hideLoader(): void { + const loader = document.getElementById('loader-modal'); + if (loader) { + loader.classList.add('hidden'); + } +} + +function showAlert(title: string, message: string): void { + const modal = document.getElementById('alert-modal'); + const titleEl = document.getElementById('alert-title'); + const msgEl = document.getElementById('alert-message'); + if (modal && titleEl && msgEl) { + titleEl.textContent = title; + msgEl.textContent = message; + modal.classList.remove('hidden'); + } +} + +function updateFileDisplay(): void { + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const deskewOptions = document.getElementById('deskew-options'); + const resultsArea = document.getElementById('results-area'); + + if (!fileDisplayArea || !fileControls || !deskewOptions || !resultsArea) + return; + + resultsArea.classList.add('hidden'); + + if (selectedFiles.length === 0) { + fileDisplayArea.innerHTML = ''; + fileControls.classList.add('hidden'); + deskewOptions.classList.add('hidden'); + return; + } + + fileControls.classList.remove('hidden'); + deskewOptions.classList.remove('hidden'); + + fileDisplayArea.innerHTML = selectedFiles + .map( + (file, index) => ` +
+
+ + ${file.name} + (${(file.size / 1024).toFixed(1)} KB) +
+ +
+ ` + ) + .join(''); + + createIcons({ icons }); + + fileDisplayArea.querySelectorAll('.remove-file').forEach((btn) => { + btn.addEventListener('click', (e) => { + const index = parseInt( + (e.currentTarget as HTMLElement).dataset.index || '0', + 10 + ); + selectedFiles.splice(index, 1); + updateFileDisplay(); + }); + }); +} + +function displayResults(result: DeskewResult): void { + const resultsArea = document.getElementById('results-area'); + const totalEl = document.getElementById('result-total'); + const correctedEl = document.getElementById('result-corrected'); + const anglesList = document.getElementById('angles-list'); + + if (!resultsArea || !totalEl || !correctedEl || !anglesList) return; + + resultsArea.classList.remove('hidden'); + totalEl.textContent = result.totalPages.toString(); + correctedEl.textContent = result.correctedPages.toString(); + + anglesList.innerHTML = result.angles + .map((angle, idx) => { + const wasCorrected = result.corrected[idx]; + const color = wasCorrected ? 'text-green-400' : 'text-gray-400'; + const icon = wasCorrected ? 'check' : 'minus'; + return ` +
+ + Page ${idx + 1}: + ${angle.toFixed(2)}° + ${wasCorrected ? '(corrected)' : ''} +
+ `; + }) + .join(''); + + createIcons({ icons }); +} + +async function processDeskew(): Promise { + if (selectedFiles.length === 0) { + showAlert('No Files', 'Please select at least one PDF file.'); + return; + } + + const thresholdSelect = document.getElementById( + 'deskew-threshold' + ) as HTMLSelectElement; + const dpiSelect = document.getElementById('deskew-dpi') as HTMLSelectElement; + + const threshold = parseFloat(thresholdSelect?.value || '0.5'); + const dpi = parseInt(dpiSelect?.value || '150', 10); + + showLoader('Initializing PyMuPDF...'); + + try { + const pdf = initPyMuPDF(); + await pdf.load(); + + for (const file of selectedFiles) { + showLoader(`Deskewing ${file.name}...`); + + const { pdf: resultPdf, result } = await pdf.deskewPdf(file, { + threshold, + dpi, + }); + + displayResults(result); + + const filename = file.name.replace('.pdf', '_deskewed.pdf'); + downloadFile(resultPdf, filename); + } + + hideLoader(); + showAlert( + 'Success', + `Deskewed ${selectedFiles.length} file(s). ${selectedFiles.length > 1 ? 'Downloads started for all files.' : ''}` + ); + } catch (error) { + hideLoader(); + console.error('Deskew error:', error); + showAlert( + 'Error', + `Failed to deskew PDF: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +function initPage(): void { + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const processBtn = document.getElementById('process-btn'); + const alertOk = document.getElementById('alert-ok'); + const backBtn = document.getElementById('back-to-tools'); + + if (fileInput) { + fileInput.addEventListener('change', () => { + if (fileInput.files) { + selectedFiles = [...selectedFiles, ...Array.from(fileInput.files)]; + updateFileDisplay(); + fileInput.value = ''; + } + }); + } + + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + if (e.dataTransfer?.files) { + const pdfFiles = Array.from(e.dataTransfer.files).filter( + (f) => f.type === 'application/pdf' + ); + selectedFiles = [...selectedFiles, ...pdfFiles]; + updateFileDisplay(); + } + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => fileInput?.click()); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + selectedFiles = []; + updateFileDisplay(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', processDeskew); + } + + if (alertOk) { + alertOk.addEventListener('click', () => { + document.getElementById('alert-modal')?.classList.add('hidden'); + }); + } + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = '/'; + }); + } + + createIcons({ icons }); +} + +document.addEventListener('DOMContentLoaded', initPage); diff --git a/src/js/logic/email-to-pdf.ts b/src/js/logic/email-to-pdf.ts index 378f1d96a..40772a36a 100644 --- a/src/js/logic/email-to-pdf.ts +++ b/src/js/logic/email-to-pdf.ts @@ -1,14 +1,16 @@ import PostalMime from 'postal-mime'; import MsgReader from '@kenjiuno/msgreader'; -import { formatBytes, escapeHtml } from '../utils/helpers.js'; +import { + formatBytes, + escapeHtml, + uint8ArrayToBase64, + sanitizeEmailHtml, + formatRawDate, +} from '../utils/helpers.js'; import type { EmailAttachment, ParsedEmail, EmailRenderOptions } from '@/types'; -// Re-export types for convenience export type { EmailAttachment, ParsedEmail, EmailRenderOptions }; -/** - * Format email address without angle brackets for cleaner display - */ function formatAddress( name: string | undefined, email: string | undefined @@ -172,80 +174,6 @@ export async function parseMsgFile(file: File): Promise { }; } -/** - * Formats a raw RFC 2822 date string into a nicer human-readable format, - * while preserving the original timezone and time. - * Example input: "Sun, 8 Jan 2017 20:37:44 +0200" - * Example output: "Sunday, January 8, 2017 at 8:37 PM (+0200)" - */ -function formatRawDate(raw: string): string { - try { - // Regex to parse RFC 2822 date parts: Day, DD Mon YYYY HH:MM:SS Timezone - const match = raw.match( - /([A-Za-z]{3}),\s+(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s+([+-]\d{4})/ - ); - - if (match) { - const [ - , - dayAbbr, - dom, - monthAbbr, - year, - hoursStr, - minsStr, - secsStr, - timezone, - ] = match; - - // Map abbreviations to full names - const days: Record = { - Sun: 'Sunday', - Mon: 'Monday', - Tue: 'Tuesday', - Wed: 'Wednesday', - Thu: 'Thursday', - Fri: 'Friday', - Sat: 'Saturday', - }; - const months: Record = { - Jan: 'January', - Feb: 'February', - Mar: 'March', - Apr: 'April', - May: 'May', - Jun: 'June', - Jul: 'July', - Aug: 'August', - Sep: 'September', - Oct: 'October', - Nov: 'November', - Dec: 'December', - }; - - const fullDay = days[dayAbbr] || dayAbbr; - const fullMonth = months[monthAbbr] || monthAbbr; - - // Convert to 12-hour format manually - let hours = parseInt(hoursStr, 10); - const ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' - - // Format timezone: +0200 -> UTC+02:00 - const tzSign = timezone.substring(0, 1); - const tzHours = timezone.substring(1, 3); - const tzMins = timezone.substring(3, 5); - const formattedTz = `UTC${tzSign}${tzHours}:${tzMins}`; - - return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`; - } - } catch (e) { - // Fallback to raw string if parsing fails - } - return raw; -} - /** * Replace CID references in HTML with base64 data URIs */ @@ -263,23 +191,13 @@ function processInlineImages( } }); - // Replace src="cid:..." return html.replace(/src=["']cid:([^"']+)["']/g, (match, cid) => { const att = cidMap.get(cid); if (att && att.content) { - // Convert Uint8Array to base64 - let binary = ''; - const len = att.content.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(att.content[i]); - } - const base64 = - typeof btoa === 'function' - ? btoa(binary) - : Buffer.from(binary, 'binary').toString('base64'); + const base64 = uint8ArrayToBase64(att.content); return `src="data:${att.contentType};base64,${base64}"`; } - return match; // Keep original if not found + return match; }); } @@ -291,12 +209,12 @@ export function renderEmailToHtml( let processedHtml = ''; if (email.htmlBody) { - processedHtml = processInlineImages(email.htmlBody, email.attachments); + const sanitizedHtml = sanitizeEmailHtml(email.htmlBody); + processedHtml = processInlineImages(sanitizedHtml, email.attachments); } else { processedHtml = `
${escapeHtml(email.textBody)}
`; } - // Format date in a human-readable way let dateStr = 'Unknown Date'; if (email.rawDateString) { dateStr = formatRawDate(email.rawDateString); @@ -329,7 +247,6 @@ export function renderEmailToHtml( ` : ''; - // Build CC/BCC rows let ccBccHtml = ''; if (includeCcBcc) { if (email.cc.length > 0) { diff --git a/src/js/logic/font-to-outline-page.ts b/src/js/logic/font-to-outline-page.ts new file mode 100644 index 000000000..83bbb2947 --- /dev/null +++ b/src/js/logic/font-to-outline-page.ts @@ -0,0 +1,222 @@ +import { showAlert } from '../ui.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { convertFileToOutlines } from '../utils/ghostscript-loader.js'; +import { icons, createIcons } from 'lucide'; +import JSZip from 'jszip'; + +interface FontToOutlineState { + files: File[]; +} + +const pageState: FontToOutlineState = { + files: [], +}; + +function resetState() { + pageState.files = []; + + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + + const toolOptions = document.getElementById('tool-options'); + if (toolOptions) toolOptions.classList.add('hidden'); + + const fileControls = document.getElementById('file-controls'); + if (fileControls) fileControls.classList.add('hidden'); + + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; +} + +async function updateUI() { + const fileDisplayArea = document.getElementById('file-display-area'); + const toolOptions = document.getElementById('tool-options'); + const fileControls = document.getElementById('file-controls'); + + if (!fileDisplayArea) return; + + fileDisplayArea.innerHTML = ''; + + if (pageState.files.length > 0) { + pageState.files.forEach((file, index) => { + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(file.size); + + infoContainer.append(nameSpan, metaSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = function () { + pageState.files.splice(index, 1); + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + }); + + createIcons({ icons }); + + if (toolOptions) toolOptions.classList.remove('hidden'); + if (fileControls) fileControls.classList.remove('hidden'); + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + if (fileControls) fileControls.classList.add('hidden'); + } +} + +function handleFileSelect(files: FileList | null) { + if (files && files.length > 0) { + const pdfFiles = Array.from(files).filter( + (f) => + f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf') + ); + if (pdfFiles.length > 0) { + pageState.files.push(...pdfFiles); + updateUI(); + } + } +} + +async function processFiles() { + if (pageState.files.length === 0) { + showAlert('No Files', 'Please select at least one PDF file.'); + return; + } + + const loaderModal = document.getElementById('loader-modal'); + const loaderText = document.getElementById('loader-text'); + + try { + if (pageState.files.length === 1) { + if (loaderModal) loaderModal.classList.remove('hidden'); + if (loaderText) + loaderText.textContent = 'Converting fonts to outlines...'; + + const file = pageState.files[0]; + const resultBlob = await convertFileToOutlines(file, (msg) => { + if (loaderText) loaderText.textContent = msg; + }); + + const baseName = file.name.replace(/\.pdf$/i, ''); + downloadFile(resultBlob, `${baseName}_outlined.pdf`); + if (loaderModal) loaderModal.classList.add('hidden'); + } else { + if (loaderModal) loaderModal.classList.remove('hidden'); + if (loaderText) loaderText.textContent = 'Processing multiple PDFs...'; + + const zip = new JSZip(); + let processedCount = 0; + + for (let i = 0; i < pageState.files.length; i++) { + const file = pageState.files[i]; + if (loaderText) + loaderText.textContent = `Processing ${i + 1}/${pageState.files.length}: ${file.name}...`; + + try { + const resultBlob = await convertFileToOutlines(file, () => {}); + const arrayBuffer = await resultBlob.arrayBuffer(); + const baseName = file.name.replace(/\.pdf$/i, ''); + zip.file(`${baseName}_outlined.pdf`, arrayBuffer); + processedCount++; + } catch (e) { + console.error(`Error processing ${file.name}:`, e); + } + } + + if (processedCount > 0) { + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'outlined_pdfs.zip'); + showAlert( + 'Success', + `Processed ${processedCount} PDFs.`, + 'success', + () => { + resetState(); + } + ); + } else { + showAlert('Error', 'No PDFs could be processed.'); + } + if (loaderModal) loaderModal.classList.add('hidden'); + } + } catch (e: unknown) { + console.error(e); + if (loaderModal) loaderModal.classList.add('hidden'); + const errorMessage = + e instanceof Error ? e.message : 'An unexpected error occurred.'; + showAlert('Error', errorMessage); + } +} + +document.addEventListener('DOMContentLoaded', function () { + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + + if (backBtn) { + backBtn.addEventListener('click', function () { + window.location.href = import.meta.env.BASE_URL; + }); + } + + if (fileInput && dropZone) { + fileInput.addEventListener('change', function (e) { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', function (e) { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', function (e) { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', function (e) { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + handleFileSelect(e.dataTransfer?.files); + }); + + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } + + if (processBtn) { + processBtn.addEventListener('click', processFiles); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', function () { + fileInput.value = ''; + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', function () { + resetState(); + }); + } +}); diff --git a/src/js/main.ts b/src/js/main.ts index 74bbf2870..989111a5a 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -271,6 +271,8 @@ const init = async () => { 'Remove Metadata': 'tools:removeMetadata', 'Change Permissions': 'tools:changePermissions', 'Email to PDF': 'tools:emailToPdf', + 'Font to Outline': 'tools:fontToOutline', + 'Deskew PDF': 'tools:deskewPdf', }; // Homepage-only tool grid rendering (not used on individual tool pages) diff --git a/src/js/utils/ghostscript-loader.ts b/src/js/utils/ghostscript-loader.ts index e08e10a9e..1f5638493 100644 --- a/src/js/utils/ghostscript-loader.ts +++ b/src/js/utils/ghostscript-loader.ts @@ -42,7 +42,7 @@ export async function convertToPdfA( gs = cachedGsModule; } else { const gsBaseUrl = getWasmBaseUrl('ghostscript'); - gs = await loadWASM({ + gs = (await loadWASM({ locateFile: (path: string) => { if (path.endsWith('.wasm')) { return gsBaseUrl + 'gs.wasm'; @@ -51,7 +51,7 @@ export async function convertToPdfA( }, print: (text: string) => console.log('[GS]', text), printErr: (text: string) => console.error('[GS Error]', text), - }) as GhostscriptModule; + })) as GhostscriptModule; cachedGsModule = gs; } @@ -76,16 +76,24 @@ export async function convertToPdfA( const response = await fetchWasmFile('ghostscript', iccFileName); if (!response.ok) { - throw new Error(`Failed to fetch ICC profile: ${iccFileName}. Ensure it is in your assets folder.`); + throw new Error( + `Failed to fetch ICC profile: ${iccFileName}. Ensure it is in your assets folder.` + ); } const iccData = new Uint8Array(await response.arrayBuffer()); - console.log('[Ghostscript] sRGB v2 ICC profile loaded:', iccData.length, 'bytes'); + console.log( + '[Ghostscript] sRGB v2 ICC profile loaded:', + iccData.length, + 'bytes' + ); gs.FS.writeFile(iccPath, iccData); console.log('[Ghostscript] sRGB ICC profile written to FS:', iccPath); - const iccHex = Array.from(iccData).map(b => b.toString(16).padStart(2, '0')).join(''); + const iccHex = Array.from(iccData) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); console.log('[Ghostscript] ICC profile hex length:', iccHex.length); const pdfaSubtype = level === 'PDF/A-1b' ? '/GTS_PDFA1' : '/GTS_PDFA'; @@ -114,7 +122,9 @@ export async function convertToPdfA( `; gs.FS.writeFile(pdfaDefPath, pdfaPS); - console.log('[Ghostscript] PDFA PostScript created with embedded ICC hex data'); + console.log( + '[Ghostscript] PDFA PostScript created with embedded ICC hex data' + ); } catch (e) { console.error('[Ghostscript] Failed to setup PDF/A assets:', e); throw new Error('Conversion failed: could not create PDF/A definition'); @@ -163,10 +173,26 @@ export async function convertToPdfA( console.log('[Ghostscript] Exit code:', exitCode); if (exitCode !== 0) { - try { gs.FS.unlink(inputPath); } catch { /* ignore */ } - try { gs.FS.unlink(outputPath); } catch { /* ignore */ } - try { gs.FS.unlink(iccPath); } catch { /* ignore */ } - try { gs.FS.unlink(pdfaDefPath); } catch { /* ignore */ } + try { + gs.FS.unlink(inputPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(outputPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(iccPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(pdfaDefPath); + } catch { + /* ignore */ + } throw new Error(`Ghostscript conversion failed with exit code ${exitCode}`); } @@ -182,14 +208,32 @@ export async function convertToPdfA( } // Cleanup - try { gs.FS.unlink(inputPath); } catch { /* ignore */ } - try { gs.FS.unlink(outputPath); } catch { /* ignore */ } - try { gs.FS.unlink(iccPath); } catch { /* ignore */ } - try { gs.FS.unlink(pdfaDefPath); } catch { /* ignore */ } + try { + gs.FS.unlink(inputPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(outputPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(iccPath); + } catch { + /* ignore */ + } + try { + gs.FS.unlink(pdfaDefPath); + } catch { + /* ignore */ + } if (level !== 'PDF/A-1b') { onProgress?.('Post-processing for transparency compliance...'); - console.log('[Ghostscript] Adding Group dictionaries to pages for transparency compliance...'); + console.log( + '[Ghostscript] Adding Group dictionaries to pages for transparency compliance...' + ); try { output = await addPageGroupDictionaries(output); @@ -202,10 +246,12 @@ export async function convertToPdfA( return output; } -async function addPageGroupDictionaries(pdfData: Uint8Array): Promise { +async function addPageGroupDictionaries( + pdfData: Uint8Array +): Promise { const pdfDoc = await PDFDocument.load(pdfData, { ignoreEncryption: true, - updateMetadata: false + updateMetadata: false, }); const catalog = pdfDoc.catalog; @@ -227,12 +273,22 @@ async function addPageGroupDictionaries(pdfData: Uint8Array): Promise { - if (obj instanceof PDFDict || (obj && typeof obj === 'object' && 'dict' in obj)) { - const dict = 'dict' in obj ? (obj as { dict: PDFDict }).dict : obj as PDFDict; + if ( + obj instanceof PDFDict || + (obj && typeof obj === 'object' && 'dict' in obj) + ) { + const dict = + 'dict' in obj ? (obj as { dict: PDFDict }).dict : (obj as PDFDict); const subtype = dict.get(PDFName.of('Subtype')); if (subtype instanceof PDFName && subtype.decodeText() === 'Form') { @@ -290,8 +353,100 @@ export async function convertFileToPdfA( const arrayBuffer = await file.arrayBuffer(); const pdfData = new Uint8Array(arrayBuffer); const result = await convertToPdfA(pdfData, level, onProgress); - // Copy to regular ArrayBuffer to avoid SharedArrayBuffer issues const copy = new Uint8Array(result.length); copy.set(result); return new Blob([copy], { type: 'application/pdf' }); -} \ No newline at end of file +} + +export async function convertFontsToOutlines( + pdfData: Uint8Array, + onProgress?: (msg: string) => void +): Promise { + onProgress?.('Loading Ghostscript...'); + + let gs: GhostscriptModule; + + if (cachedGsModule) { + gs = cachedGsModule; + } else { + const gsBaseUrl = getWasmBaseUrl('ghostscript'); + gs = (await loadWASM({ + locateFile: (path: string) => { + if (path.endsWith('.wasm')) { + return gsBaseUrl + 'gs.wasm'; + } + return path; + }, + print: (text: string) => console.log('[GS]', text), + printErr: (text: string) => console.error('[GS Error]', text), + })) as GhostscriptModule; + cachedGsModule = gs; + } + + const inputPath = '/tmp/input.pdf'; + const outputPath = '/tmp/output.pdf'; + + gs.FS.writeFile(inputPath, pdfData); + + onProgress?.('Converting fonts to outlines...'); + + const args = [ + '-dNOSAFER', + '-dBATCH', + '-dNOPAUSE', + '-sDEVICE=pdfwrite', + '-dNoOutputFonts', + '-dCompressPages=true', + '-dAutoRotatePages=/None', + `-sOutputFile=${outputPath}`, + inputPath, + ]; + + let exitCode: number; + try { + exitCode = gs.callMain(args); + } catch (e) { + try { + gs.FS.unlink(inputPath); + } catch {} + throw new Error(`Ghostscript threw an exception: ${e}`); + } + + if (exitCode !== 0) { + try { + gs.FS.unlink(inputPath); + } catch {} + try { + gs.FS.unlink(outputPath); + } catch {} + throw new Error(`Ghostscript conversion failed with exit code ${exitCode}`); + } + + let output: Uint8Array; + try { + output = gs.FS.readFile(outputPath); + } catch (e) { + throw new Error('Ghostscript did not produce output file'); + } + + try { + gs.FS.unlink(inputPath); + } catch {} + try { + gs.FS.unlink(outputPath); + } catch {} + + return output; +} + +export async function convertFileToOutlines( + file: File, + onProgress?: (msg: string) => void +): Promise { + const arrayBuffer = await file.arrayBuffer(); + const pdfData = new Uint8Array(arrayBuffer); + const result = await convertFontsToOutlines(pdfData, onProgress); + const copy = new Uint8Array(result.length); + copy.set(result); + return new Blob([copy], { type: 'application/pdf' }); +} diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index b760234a0..b5afd5fa8 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -306,3 +306,157 @@ export function escapeHtml(text: string): string { }; return text.replace(/[&<>"']/g, (m) => map[m]); } + +export function uint8ArrayToBase64(bytes: Uint8Array): string { + const CHUNK_SIZE = 0x8000; + const chunks: string[] = []; + for (let i = 0; i < bytes.length; i += CHUNK_SIZE) { + const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length)); + chunks.push(String.fromCharCode(...chunk)); + } + return btoa(chunks.join('')); +} + +export function sanitizeEmailHtml(html: string): string { + if (!html) return html; + + let sanitized = html; + + sanitized = sanitized.replace(/]*>[\s\S]*?<\/head>/gi, ''); + sanitized = sanitized.replace(/]*>[\s\S]*?<\/style>/gi, ''); + sanitized = sanitized.replace(/]*>[\s\S]*?<\/script>/gi, ''); + sanitized = sanitized.replace(/]*>/gi, ''); + sanitized = sanitized.replace(/\s+style=["'][^"']*["']/gi, ''); + sanitized = sanitized.replace(/\s+class=["'][^"']*["']/gi, ''); + sanitized = sanitized.replace(/\s+data-[a-z-]+=["'][^"']*["']/gi, ''); + sanitized = sanitized.replace( + /]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi, + '' + ); + sanitized = sanitized.replace( + /href=["']https?:\/\/[^"']*safelinks\.protection\.outlook\.com[^"']*url=([^&"']+)[^"']*["']/gi, + (match, encodedUrl) => { + try { + const decodedUrl = decodeURIComponent(encodedUrl); + return `href="${decodedUrl}"`; + } catch { + return match; + } + } + ); + sanitized = sanitized.replace(/\s+originalsrc=["'][^"']*["']/gi, ''); + sanitized = sanitized.replace( + /href=["']([^"']{500,})["']/gi, + (match, url) => { + const baseUrl = url.split('?')[0]; + if (baseUrl && baseUrl.length < 200) { + return `href="${baseUrl}"`; + } + return `href="${url.substring(0, 200)}"`; + } + ); + + sanitized = sanitized.replace( + /\s+(cellpadding|cellspacing|bgcolor|border|valign|align|width|height|role|dir|id)=["'][^"']*["']/gi, + '' + ); + sanitized = sanitized.replace(/<\/?table[^>]*>/gi, '
'); + sanitized = sanitized.replace(/<\/?tbody[^>]*>/gi, ''); + sanitized = sanitized.replace(/<\/?thead[^>]*>/gi, ''); + sanitized = sanitized.replace(/<\/?tfoot[^>]*>/gi, ''); + sanitized = sanitized.replace(/]*>/gi, '
'); + sanitized = sanitized.replace(/<\/tr>/gi, '
'); + sanitized = sanitized.replace(/]*>/gi, ' '); + sanitized = sanitized.replace(/<\/td>/gi, ' '); + sanitized = sanitized.replace(/]*>/gi, ' '); + sanitized = sanitized.replace(/<\/th>/gi, ' '); + sanitized = sanitized.replace(/
\s*<\/div>/gi, ''); + sanitized = sanitized.replace(/\s*<\/span>/gi, ''); + sanitized = sanitized.replace(/(
)+/gi, '
'); + sanitized = sanitized.replace(/(<\/div>)+/gi, '
'); + sanitized = sanitized.replace( + /]*href=["']\s*["'][^>]*>([^<]*)<\/a>/gi, + '$1' + ); + + const MAX_HTML_SIZE = 100000; + if (sanitized.length > MAX_HTML_SIZE) { + const truncateAt = sanitized.lastIndexOf('
', MAX_HTML_SIZE); + if (truncateAt > MAX_HTML_SIZE / 2) { + sanitized = sanitized.substring(0, truncateAt) + '
'; + } else { + sanitized = sanitized.substring(0, MAX_HTML_SIZE) + '...'; + } + } + + return sanitized; +} + +/** + * Formats a raw RFC 2822 date string into a nicer human-readable format, + * while preserving the original timezone and time. + * Example input: "Sun, 8 Jan 2017 20:37:44 +0200" + * Example output: "Sunday, January 8, 2017 at 8:37 PM (+0200)" + */ +export function formatRawDate(raw: string): string { + try { + const match = raw.match( + /([A-Za-z]{3}),\s+(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s+([+-]\d{4})/ + ); + + if (match) { + const [ + , + dayAbbr, + dom, + monthAbbr, + year, + hoursStr, + minsStr, + secsStr, + timezone, + ] = match; + + const days: Record = { + Sun: 'Sunday', + Mon: 'Monday', + Tue: 'Tuesday', + Wed: 'Wednesday', + Thu: 'Thursday', + Fri: 'Friday', + Sat: 'Saturday', + }; + const months: Record = { + Jan: 'January', + Feb: 'February', + Mar: 'March', + Apr: 'April', + May: 'May', + Jun: 'June', + Jul: 'July', + Aug: 'August', + Sep: 'September', + Oct: 'October', + Nov: 'November', + Dec: 'December', + }; + + const fullDay = days[dayAbbr] || dayAbbr; + const fullMonth = months[monthAbbr] || monthAbbr; + + let hours = parseInt(hoursStr, 10); + const ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; + const tzSign = timezone.substring(0, 1); + const tzHours = timezone.substring(1, 3); + const tzMins = timezone.substring(3, 5); + const formattedTz = `UTC${tzSign}${tzHours}:${tzMins}`; + + return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`; + } + } catch (e) { + // Fallback to raw string if parsing fails + } + return raw; +} diff --git a/src/pages/deskew-pdf.html b/src/pages/deskew-pdf.html new file mode 100644 index 000000000..650dfbf83 --- /dev/null +++ b/src/pages/deskew-pdf.html @@ -0,0 +1,677 @@ + + + + + + + Deskew PDF Online Free - Straighten Scanned PDFs | BentoPDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +

+ Deskew PDF Free Online - Straighten Tilted Scans +

+

+ Automatically detect and correct skewed pages in scanned PDFs. Uses + advanced image processing to straighten tilted documents. +

+ +
+
+ +

+ Click to select files or drag + and drop +

+

One or more PDF files

+

+ Your files never leave your device. +

+
+ +
+ + + +
+ + + + +
+
+ + + + + +
+

+ How It Works +

+
+
+
+ 1 +
+
+

Upload PDF

+

+ Select your scanned PDF with tilted pages +

+
+
+
+
+ 2 +
+
+

Auto-Detect

+

OpenCV analyzes and detects skew angles

+
+
+
+
+ 3 +
+
+

Download

+

Get your straightened PDF instantly

+
+
+
+
+ +
+

+ Related PDF Tools +

+ +
+ +
+

+ Frequently Asked Questions +

+
+
+ + What is PDF deskewing? + + +

+ Deskewing is the process of correcting tilted or rotated pages in + scanned documents. When you scan a document, it's common for pages + to be slightly skewed. This tool automatically detects and corrects + that skew. +

+
+
+ + How accurate is the skew detection? + + +

+ Our tool uses OpenCV's advanced image processing algorithms to + detect skew angles with high precision. It works best on documents + with clear text content. +

+
+
+ + Are my files private? + + +

+ Yes! All processing happens entirely in your browser using + WebAssembly. Your files never leave your device. +

+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/src/pages/font-to-outline.html b/src/pages/font-to-outline.html new file mode 100644 index 000000000..f8ad7585d --- /dev/null +++ b/src/pages/font-to-outline.html @@ -0,0 +1,667 @@ + + + + + + + + Font to Outline PDF Online Free - Convert Fonts to Paths | BentoPDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +

+ Font to Outline Free Online - Convert Fonts to Paths +

+

+ Convert all fonts in your PDF to vector outlines/paths. Ensures + consistent rendering across all devices regardless of font + availability. +

+ +
+
+ +

+ Click to select files or drag + and drop +

+

One or more PDF files

+

+ Your files never leave your device. +

+
+ +
+ + + +
+ + +
+
+ + + + + +
+

+ How It Works +

+
+
+
+ 1 +
+
+

Upload PDF

+

+ Select your PDF file with embedded fonts +

+
+
+
+
+ 2 +
+
+

Convert

+

+ Ghostscript converts all fonts to vector paths +

+
+
+
+
+ 3 +
+
+

Download

+

Get your font-independent PDF instantly

+
+
+
+
+ +
+

+ Related PDF Tools +

+ +
+ +
+

+ Frequently Asked Questions +

+
+
+ + What does "font to outline" mean? + + +

+ Converting fonts to outlines means transforming text characters from + font-based representations into vector paths/curves. This ensures + the PDF looks identical on any device, even if the original fonts + aren't installed. +

+
+
+ + When should I use this tool? + + +

+ Use this tool when preparing PDFs for professional printing, sharing + documents with special fonts, or when you need to ensure consistent + appearance across different systems and devices. +

+
+
+ + Will the text still be selectable? + + +

+ No. After conversion, text becomes vector graphics and is no longer + selectable or searchable. If you need searchable text, consider + using the OCR tool after conversion. +

+
+
+ + Are my files private? + + +

+ Yes! All processing happens entirely in your browser using + WebAssembly. Your files never leave your device. +

+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/vite.config.ts b/vite.config.ts index e3e4c424a..c4caa121a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -423,6 +423,11 @@ export default defineConfig(({ mode }) => { 'src/pages/validate-signature-pdf.html' ), 'email-to-pdf': resolve(__dirname, 'src/pages/email-to-pdf.html'), + 'font-to-outline': resolve( + __dirname, + 'src/pages/font-to-outline.html' + ), + 'deskew-pdf': resolve(__dirname, 'src/pages/deskew-pdf.html'), }, }, }, From c5799954dce35968389263a179a59a399c069ad1 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Sat, 10 Jan 2026 13:09:52 +0530 Subject: [PATCH 15/39] fix(ocr): improve text layer alignment with width-based font sizing - Create new hocr-transform.ts utility for parsing hOCR output - Add line-aware text processing with baseline and rotation support - Implement width-based font size calculation to match word bounding boxes - Fix text selection not covering full characters issue - Add proper type definitions for OcrLine, OcrPage, WordTransform - Support RTL languages and CJK word break handling --- src/js/logic/ocr-pdf-page.ts | 1058 ++++++++++++++++++-------------- src/js/types/ocr-pdf-type.ts | 46 +- src/js/utils/hocr-transform.ts | 266 ++++++++ 3 files changed, 898 insertions(+), 472 deletions(-) create mode 100644 src/js/utils/hocr-transform.ts diff --git a/src/js/logic/ocr-pdf-page.ts b/src/js/logic/ocr-pdf-page.ts index bdadf7aa2..422ae66c3 100644 --- a/src/js/logic/ocr-pdf-page.ts +++ b/src/js/logic/ocr-pdf-page.ts @@ -2,556 +2,680 @@ import { tesseractLanguages } from '../config/tesseract-languages.js'; import { showAlert } from '../ui.js'; import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js'; import Tesseract from 'tesseract.js'; -import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib'; +import { + PDFDocument as PDFLibDocument, + StandardFonts, + rgb, + PDFFont, +} from 'pdf-lib'; import fontkit from '@pdf-lib/fontkit'; import { icons, createIcons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import { getFontForLanguage } from '../utils/font-loader.js'; -import { OcrWord, OcrState } from '@/types'; - -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); +import { OcrState, OcrLine, OcrPage } from '@/types'; +import { + parseHocrDocument, + calculateWordTransform, + calculateSpaceTransform, +} from '../utils/hocr-transform.js'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); const pageState: OcrState = { - file: null, - searchablePdfBytes: null, + file: null, + searchablePdfBytes: null, }; const whitelistPresets: Record = { - alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"', - 'numbers-currency': '0123456789$€£¥.,- ', - 'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ', - 'numbers-only': '0123456789', - invoice: '0123456789$.,/-#: ', - forms: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:', + alphanumeric: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"', + 'numbers-currency': '0123456789$€£¥.,- ', + 'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ', + 'numbers-only': '0123456789', + invoice: '0123456789$.,/-#: ', + forms: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:', }; -function parseHOCR(hocrText: string): OcrWord[] { - const parser = new DOMParser(); - const doc = parser.parseFromString(hocrText, 'text/html'); - const words: OcrWord[] = []; - - const wordElements = doc.querySelectorAll('.ocrx_word'); - - wordElements.forEach(function (wordEl) { - const titleAttr = wordEl.getAttribute('title'); - const text = wordEl.textContent?.trim() || ''; - - if (!titleAttr || !text) return; - - const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/); - const confMatch = titleAttr.match(/x_wconf (\d+)/); - - if (bboxMatch) { - words.push({ - text: text, - bbox: { - x0: parseInt(bboxMatch[1]), - y0: parseInt(bboxMatch[2]), - x1: parseInt(bboxMatch[3]), - y1: parseInt(bboxMatch[4]), - }, - confidence: confMatch ? parseInt(confMatch[1]) : 0, +function drawOcrTextLayer( + page: ReturnType, + ocrPage: OcrPage, + pageHeight: number, + primaryFont: PDFFont, + latinFont: PDFFont +): void { + ocrPage.lines.forEach(function (line: OcrLine) { + const words = line.words; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const text = word.text.replace( + /[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, + '' + ); + + if (!text.trim()) continue; + + const hasNonLatin = /[^\u0000-\u007F]/.test(text); + const font = hasNonLatin ? primaryFont : latinFont; + + if (!font) { + console.warn('Font not available for text: "' + text + '"'); + continue; + } + + const transform = calculateWordTransform( + word, + line, + pageHeight, + (txt: string, size: number) => { + try { + return font.widthOfTextAtSize(txt, size); + } catch { + return 0; + } + } + ); + + if (transform.fontSize <= 0) continue; + + try { + page.drawText(text, { + x: transform.x, + y: transform.y, + font, + size: transform.fontSize, + color: rgb(0, 0, 0), + opacity: 0, + }); + } catch (error) { + console.warn(`Could not draw text "${text}":`, error); + } + + if (line.injectWordBreaks && i < words.length - 1) { + const nextWord = words[i + 1]; + const spaceTransform = calculateSpaceTransform( + word, + nextWord, + line, + pageHeight, + (size: number) => { + try { + return font.widthOfTextAtSize(' ', size); + } catch { + return 0; + } + } + ); + + if (spaceTransform && spaceTransform.horizontalScale > 0.1) { + try { + page.drawText(' ', { + x: spaceTransform.x, + y: spaceTransform.y, + font, + size: spaceTransform.fontSize, + color: rgb(0, 0, 0), + opacity: 0, }); + } catch { + console.warn(`Could not draw space between words`); + } } - }); - - return words; + } + } + }); } function binarizeCanvas(ctx: CanvasRenderingContext2D) { - const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); - const data = imageData.data; - for (let i = 0; i < data.length; i += 4) { - const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; - const color = brightness > 128 ? 255 : 0; - data[i] = data[i + 1] = data[i + 2] = color; - } - ctx.putImageData(imageData, 0, 0); + const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const brightness = + 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; + const color = brightness > 128 ? 255 : 0; + data[i] = data[i + 1] = data[i + 2] = color; + } + ctx.putImageData(imageData, 0, 0); } function updateProgress(status: string, progress: number) { - const progressBar = document.getElementById('progress-bar'); - const progressStatus = document.getElementById('progress-status'); - const progressLog = document.getElementById('progress-log'); + const progressBar = document.getElementById('progress-bar'); + const progressStatus = document.getElementById('progress-status'); + const progressLog = document.getElementById('progress-log'); - if (!progressBar || !progressStatus || !progressLog) return; + if (!progressBar || !progressStatus || !progressLog) return; - progressStatus.textContent = status; - progressBar.style.width = `${Math.min(100, progress * 100)}%`; + progressStatus.textContent = status; + progressBar.style.width = `${Math.min(100, progress * 100)}%`; - const logMessage = `Status: ${status}`; - progressLog.textContent += logMessage + '\n'; - progressLog.scrollTop = progressLog.scrollHeight; + const logMessage = `Status: ${status}`; + progressLog.textContent += logMessage + '\n'; + progressLog.scrollTop = progressLog.scrollHeight; } function resetState() { - pageState.file = null; - pageState.searchablePdfBytes = null; + pageState.file = null; + pageState.searchablePdfBytes = null; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const toolOptions = document.getElementById('tool-options'); - if (toolOptions) toolOptions.classList.add('hidden'); + const toolOptions = document.getElementById('tool-options'); + if (toolOptions) toolOptions.classList.add('hidden'); - const ocrProgress = document.getElementById('ocr-progress'); - if (ocrProgress) ocrProgress.classList.add('hidden'); + const ocrProgress = document.getElementById('ocr-progress'); + if (ocrProgress) ocrProgress.classList.add('hidden'); - const ocrResults = document.getElementById('ocr-results'); - if (ocrResults) ocrResults.classList.add('hidden'); + const ocrResults = document.getElementById('ocr-results'); + if (ocrResults) ocrResults.classList.add('hidden'); - const progressLog = document.getElementById('progress-log'); - if (progressLog) progressLog.textContent = ''; + const progressLog = document.getElementById('progress-log'); + if (progressLog) progressLog.textContent = ''; - const progressBar = document.getElementById('progress-bar'); - if (progressBar) progressBar.style.width = '0%'; + const progressBar = document.getElementById('progress-bar'); + if (progressBar) progressBar.style.width = '0%'; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - // Reset selected languages - const langCheckboxes = document.querySelectorAll('.lang-checkbox') as NodeListOf; - langCheckboxes.forEach(function (cb) { cb.checked = false; }); + // Reset selected languages + const langCheckboxes = document.querySelectorAll( + '.lang-checkbox' + ) as NodeListOf; + langCheckboxes.forEach(function (cb) { + cb.checked = false; + }); - const selectedLangsDisplay = document.getElementById('selected-langs-display'); - if (selectedLangsDisplay) selectedLangsDisplay.textContent = 'None'; + const selectedLangsDisplay = document.getElementById( + 'selected-langs-display' + ); + if (selectedLangsDisplay) selectedLangsDisplay.textContent = 'None'; - const processBtn = document.getElementById('process-btn') as HTMLButtonElement; - if (processBtn) processBtn.disabled = true; + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + if (processBtn) processBtn.disabled = true; } async function runOCR() { - const selectedLangs = Array.from( - document.querySelectorAll('.lang-checkbox:checked') - ).map(function (cb) { return (cb as HTMLInputElement).value; }); - - const scale = parseFloat( - (document.getElementById('ocr-resolution') as HTMLSelectElement).value + const selectedLangs = Array.from( + document.querySelectorAll('.lang-checkbox:checked') + ).map(function (cb) { + return (cb as HTMLInputElement).value; + }); + + const scale = parseFloat( + (document.getElementById('ocr-resolution') as HTMLSelectElement).value + ); + const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement) + .checked; + const whitelist = ( + document.getElementById('ocr-whitelist') as HTMLInputElement + ).value; + + if (selectedLangs.length === 0) { + showAlert( + 'No Languages Selected', + 'Please select at least one language for OCR.' ); - const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement).checked; - const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement).value; - - if (selectedLangs.length === 0) { - showAlert('No Languages Selected', 'Please select at least one language for OCR.'); - return; - } - - if (!pageState.file) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - - const langString = selectedLangs.join('+'); - - const toolOptions = document.getElementById('tool-options'); - const ocrProgress = document.getElementById('ocr-progress'); - - if (toolOptions) toolOptions.classList.add('hidden'); - if (ocrProgress) ocrProgress.classList.remove('hidden'); - - try { - const worker = await Tesseract.createWorker(langString, 1, { - logger: function (m: { status: string; progress: number }) { - updateProgress(m.status, m.progress || 0); - }, - }); - - await worker.setParameters({ - tessjs_create_hocr: '1', - tessedit_pageseg_mode: Tesseract.PSM.AUTO, - }); - - if (whitelist) { - await worker.setParameters({ - tessedit_char_whitelist: whitelist, - }); - } + return; + } - const arrayBuffer = await pageState.file.arrayBuffer(); - const pdf = await getPDFDocument({ data: arrayBuffer }).promise; - const newPdfDoc = await PDFLibDocument.create(); + if (!pageState.file) { + showAlert('No File', 'Please upload a PDF file first.'); + return; + } - newPdfDoc.registerFontkit(fontkit); + const langString = selectedLangs.join('+'); - updateProgress('Loading fonts...', 0); + const toolOptions = document.getElementById('tool-options'); + const ocrProgress = document.getElementById('ocr-progress'); - const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor']; - const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin']; - const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr']; + if (toolOptions) toolOptions.classList.add('hidden'); + if (ocrProgress) ocrProgress.classList.remove('hidden'); - const primaryLang = selectedLangs.find(function (l) { return priorityLangs.includes(l); }) || selectedLangs[0] || 'eng'; - - const hasCJK = selectedLangs.some(function (l) { return cjkLangs.includes(l); }); - const hasIndic = selectedLangs.some(function (l) { return indicLangs.includes(l); }); - const hasLatin = selectedLangs.some(function (l) { return !priorityLangs.includes(l); }) || selectedLangs.includes('eng'); - const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK; - - let primaryFont; - let latinFont; - - try { - if (isIndicPlusLatin) { - const [scriptFontBytes, latinFontBytes] = await Promise.all([ - getFontForLanguage(primaryLang), - getFontForLanguage('eng') - ]); - primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false }); - latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false }); - } else { - const fontBytes = await getFontForLanguage(primaryLang); - primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false }); - latinFont = primaryFont; - } - } catch (e) { - console.error('Font loading failed, falling back to Helvetica', e); - primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica); - latinFont = primaryFont; - showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.'); - } - - let fullText = ''; - - for (let i = 1; i <= pdf.numPages; i++) { - updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages); - - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale }); - - const canvas = document.createElement('canvas'); - canvas.width = viewport.width; - canvas.height = viewport.height; - const context = canvas.getContext('2d')!; - - await page.render({ canvasContext: context, viewport, canvas }).promise; - - if (binarize) { - binarizeCanvas(context); - } - - const result = await worker.recognize(canvas, {}, { text: true, hocr: true }); - const data = result.data; - - const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); - - const pngImageBytes = await new Promise(function (resolve) { - canvas.toBlob(function (blob) { - const reader = new FileReader(); - reader.onload = function () { - resolve(new Uint8Array(reader.result as ArrayBuffer)); - }; - reader.readAsArrayBuffer(blob!); - }, 'image/png'); - }); + try { + const worker = await Tesseract.createWorker(langString, 1, { + logger: function (m: { status: string; progress: number }) { + updateProgress(m.status, m.progress || 0); + }, + }); - const pngImage = await newPdfDoc.embedPng(pngImageBytes); - newPage.drawImage(pngImage, { - x: 0, - y: 0, - width: viewport.width, - height: viewport.height, - }); + await worker.setParameters({ + tessjs_create_hocr: '1', + tessedit_pageseg_mode: Tesseract.PSM.AUTO, + }); - if (data.hocr) { - const words = parseHOCR(data.hocr); - - words.forEach(function (word: OcrWord) { - const { x0, y0, x1, y1 } = word.bbox; - const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, ''); - - if (!text.trim()) return; - - const hasNonLatin = /[^\u0000-\u007F]/.test(text); - const font = hasNonLatin ? primaryFont : latinFont; - - if (!font) { - console.warn(`Font not available for text: "${text}"`); - return; - } - - const bboxWidth = x1 - x0; - const bboxHeight = y1 - y0; - - if (bboxWidth <= 0 || bboxHeight <= 0) { - return; - } - - let fontSize = bboxHeight * 0.9; - try { - let textWidth = font.widthOfTextAtSize(text, fontSize); - while (textWidth > bboxWidth && fontSize > 1) { - fontSize -= 0.5; - textWidth = font.widthOfTextAtSize(text, fontSize); - } - } catch (error) { - console.warn(`Could not calculate text width for "${text}":`, error); - return; - } - - try { - newPage.drawText(text, { - x: x0, - y: viewport.height - y1 + (bboxHeight - fontSize) / 2, - font, - size: fontSize, - color: rgb(0, 0, 0), - opacity: 0, - }); - } catch (error) { - console.warn(`Could not draw text "${text}":`, error); - } - }); - } + if (whitelist) { + await worker.setParameters({ + tessedit_char_whitelist: whitelist, + }); + } - fullText += data.text + '\n\n'; - } + const arrayBuffer = await pageState.file.arrayBuffer(); + const pdf = await getPDFDocument({ data: arrayBuffer }).promise; + const newPdfDoc = await PDFLibDocument.create(); + + newPdfDoc.registerFontkit(fontkit); + + updateProgress('Loading fonts...', 0); + + const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor']; + const indicLangs = [ + 'hin', + 'ben', + 'guj', + 'kan', + 'mal', + 'ori', + 'pan', + 'tam', + 'tel', + 'sin', + ]; + const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr']; + + const primaryLang = + selectedLangs.find(function (l) { + return priorityLangs.includes(l); + }) || + selectedLangs[0] || + 'eng'; + + const hasCJK = selectedLangs.some(function (l) { + return cjkLangs.includes(l); + }); + const hasIndic = selectedLangs.some(function (l) { + return indicLangs.includes(l); + }); + const hasLatin = + selectedLangs.some(function (l) { + return !priorityLangs.includes(l); + }) || selectedLangs.includes('eng'); + const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK; - await worker.terminate(); + let primaryFont; + let latinFont; - pageState.searchablePdfBytes = await newPdfDoc.save(); + try { + if (isIndicPlusLatin) { + const [scriptFontBytes, latinFontBytes] = await Promise.all([ + getFontForLanguage(primaryLang), + getFontForLanguage('eng'), + ]); + primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { + subset: false, + }); + latinFont = await newPdfDoc.embedFont(latinFontBytes, { + subset: false, + }); + } else { + const fontBytes = await getFontForLanguage(primaryLang); + primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false }); + latinFont = primaryFont; + } + } catch (e) { + console.error('Font loading failed, falling back to Helvetica', e); + primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica); + latinFont = primaryFont; + showAlert( + 'Font Warning', + 'Could not load the specific font for this language. Some characters may not appear correctly.' + ); + } - const ocrResults = document.getElementById('ocr-results'); - if (ocrProgress) ocrProgress.classList.add('hidden'); - if (ocrResults) ocrResults.classList.remove('hidden'); + let fullText = ''; + + for (let i = 1; i <= pdf.numPages; i++) { + updateProgress( + `Processing page ${i} of ${pdf.numPages}`, + (i - 1) / pdf.numPages + ); + + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale }); + + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const context = canvas.getContext('2d')!; + + await page.render({ canvasContext: context, viewport, canvas }).promise; + + if (binarize) { + binarizeCanvas(context); + } + + const result = await worker.recognize( + canvas, + {}, + { text: true, hocr: true } + ); + const data = result.data; + + const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); + + const pngImageBytes = await new Promise(function (resolve) { + canvas.toBlob(function (blob) { + const reader = new FileReader(); + reader.onload = function () { + resolve(new Uint8Array(reader.result as ArrayBuffer)); + }; + reader.readAsArrayBuffer(blob!); + }, 'image/png'); + }); + + const pngImage = await newPdfDoc.embedPng(pngImageBytes); + newPage.drawImage(pngImage, { + x: 0, + y: 0, + width: viewport.width, + height: viewport.height, + }); + + if (data.hocr) { + const ocrPage = parseHocrDocument(data.hocr); + drawOcrTextLayer( + newPage, + ocrPage, + viewport.height, + primaryFont, + latinFont + ); + } + + fullText += data.text + '\n\n'; + } - createIcons({ icons }); + await worker.terminate(); - const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement; - if (textOutput) textOutput.value = fullText.trim(); + pageState.searchablePdfBytes = await newPdfDoc.save(); - } catch (e) { - console.error(e); - showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.'); - if (toolOptions) toolOptions.classList.remove('hidden'); - if (ocrProgress) ocrProgress.classList.add('hidden'); - } + const ocrResults = document.getElementById('ocr-results'); + if (ocrProgress) ocrProgress.classList.add('hidden'); + if (ocrResults) ocrResults.classList.remove('hidden'); + + createIcons({ icons }); + + const textOutput = document.getElementById( + 'ocr-text-output' + ) as HTMLTextAreaElement; + if (textOutput) textOutput.value = fullText.trim(); + } catch (e) { + console.error(e); + showAlert( + 'OCR Error', + 'An error occurred during the OCR process. The worker may have failed to load. Please try again.' + ); + if (toolOptions) toolOptions.classList.remove('hidden'); + if (ocrProgress) ocrProgress.classList.add('hidden'); + } } async function updateUI() { - const fileDisplayArea = document.getElementById('file-display-area'); - const toolOptions = document.getElementById('tool-options'); + const fileDisplayArea = document.getElementById('file-display-area'); + const toolOptions = document.getElementById('tool-options'); - if (!fileDisplayArea) return; + if (!fileDisplayArea) return; - fileDisplayArea.innerHTML = ''; + fileDisplayArea.innerHTML = ''; - if (pageState.file) { - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + if (pageState.file) { + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = pageState.file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = formatBytes(pageState.file.size); + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(pageState.file.size); - infoContainer.append(nameSpan, metaSpan); + infoContainer.append(nameSpan, metaSpan); - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = function () { - resetState(); - }; + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = function () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - if (toolOptions) toolOptions.classList.remove('hidden'); - } else { - if (toolOptions) toolOptions.classList.add('hidden'); - } + if (toolOptions) toolOptions.classList.remove('hidden'); + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } function handleFileSelect(files: FileList | null) { - if (files && files.length > 0) { - const file = files[0]; - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { - pageState.file = file; - updateUI(); - } + if (files && files.length > 0) { + const file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } function populateLanguageList() { - const langList = document.getElementById('lang-list'); - if (!langList) return; - - langList.innerHTML = ''; - - Object.entries(tesseractLanguages).forEach(function ([code, name]) { - const label = document.createElement('label'); - label.className = 'flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer'; - - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.value = code; - checkbox.className = 'lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500'; - - label.append(checkbox); - label.append(document.createTextNode(' ' + name)); - langList.appendChild(label); - }); + const langList = document.getElementById('lang-list'); + if (!langList) return; + + langList.innerHTML = ''; + + Object.entries(tesseractLanguages).forEach(function ([code, name]) { + const label = document.createElement('label'); + label.className = + 'flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = code; + checkbox.className = + 'lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500'; + + label.append(checkbox); + label.append(document.createTextNode(' ' + name)); + langList.appendChild(label); + }); } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn') as HTMLButtonElement; - const backBtn = document.getElementById('back-to-tools'); - const langSearch = document.getElementById('lang-search') as HTMLInputElement; - const langList = document.getElementById('lang-list'); - const selectedLangsDisplay = document.getElementById('selected-langs-display'); - const presetSelect = document.getElementById('whitelist-preset') as HTMLSelectElement; - const whitelistInput = document.getElementById('ocr-whitelist') as HTMLInputElement; - const copyBtn = document.getElementById('copy-text-btn'); - const downloadTxtBtn = document.getElementById('download-txt-btn'); - const downloadPdfBtn = document.getElementById('download-searchable-pdf'); - - populateLanguageList(); - - if (backBtn) { - backBtn.addEventListener('click', function () { - window.location.href = import.meta.env.BASE_URL; - }); - } - - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); - - dropZone.addEventListener('dragover', function (e) { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', function (e) { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', function (e) { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const pdfFiles = Array.from(files).filter(function (f) { - return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); - }); - if (pdfFiles.length > 0) { - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(pdfFiles[0]); - handleFileSelect(dataTransfer.files); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - // Language search - if (langSearch && langList) { - langSearch.addEventListener('input', function () { - const searchTerm = langSearch.value.toLowerCase(); - langList.querySelectorAll('label').forEach(function (label) { - (label as HTMLElement).style.display = label.textContent?.toLowerCase().includes(searchTerm) ? '' : 'none'; - }); - }); - - langList.addEventListener('change', function () { - const selected = Array.from( - langList.querySelectorAll('.lang-checkbox:checked') - ).map(function (cb) { - return tesseractLanguages[(cb as HTMLInputElement).value as keyof typeof tesseractLanguages]; - }); - - if (selectedLangsDisplay) { - selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None'; - } - - if (processBtn) { - processBtn.disabled = selected.length === 0; - } - }); - } + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + const backBtn = document.getElementById('back-to-tools'); + const langSearch = document.getElementById('lang-search') as HTMLInputElement; + const langList = document.getElementById('lang-list'); + const selectedLangsDisplay = document.getElementById( + 'selected-langs-display' + ); + const presetSelect = document.getElementById( + 'whitelist-preset' + ) as HTMLSelectElement; + const whitelistInput = document.getElementById( + 'ocr-whitelist' + ) as HTMLInputElement; + const copyBtn = document.getElementById('copy-text-btn'); + const downloadTxtBtn = document.getElementById('download-txt-btn'); + const downloadPdfBtn = document.getElementById('download-searchable-pdf'); + + populateLanguageList(); + + if (backBtn) { + backBtn.addEventListener('click', function () { + window.location.href = import.meta.env.BASE_URL; + }); + } - // Whitelist preset - if (presetSelect && whitelistInput) { - presetSelect.addEventListener('change', function () { - const preset = presetSelect.value; - if (preset && preset !== 'custom') { - whitelistInput.value = whitelistPresets[preset] || ''; - whitelistInput.disabled = true; - } else { - whitelistInput.disabled = false; - if (preset === '') { - whitelistInput.value = ''; - } - } - }); - } + if (fileInput && dropZone) { + fileInput.addEventListener('change', function (e) { + handleFileSelect((e.target as HTMLInputElement).files); + }); - // Details toggle - document.querySelectorAll('details').forEach(function (details) { - details.addEventListener('toggle', function () { - const icon = details.querySelector('.details-icon') as HTMLElement; - if (icon) { - icon.style.transform = (details as HTMLDetailsElement).open ? 'rotate(180deg)' : 'rotate(0deg)'; - } - }); + dropZone.addEventListener('dragover', function (e) { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); }); - // Process button - if (processBtn) { - processBtn.addEventListener('click', runOCR); - } + dropZone.addEventListener('dragleave', function (e) { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); - // Copy button - if (copyBtn) { - copyBtn.addEventListener('click', function () { - const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement; - if (textOutput) { - navigator.clipboard.writeText(textOutput.value).then(function () { - copyBtn.innerHTML = ''; - createIcons({ icons }); - - setTimeout(function () { - copyBtn.innerHTML = ''; - createIcons({ icons }); - }, 2000); - }); - } + dropZone.addEventListener('drop', function (e) { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const pdfFiles = Array.from(files).filter(function (f) { + return ( + f.type === 'application/pdf' || + f.name.toLowerCase().endsWith('.pdf') + ); }); - } + if (pdfFiles.length > 0) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(pdfFiles[0]); + handleFileSelect(dataTransfer.files); + } + } + }); - // Download txt - if (downloadTxtBtn) { - downloadTxtBtn.addEventListener('click', function () { - const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement; - if (textOutput) { - const blob = new Blob([textOutput.value], { type: 'text/plain' }); - downloadFile(blob, 'ocr-text.txt'); - } - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } + + // Language search + if (langSearch && langList) { + langSearch.addEventListener('input', function () { + const searchTerm = langSearch.value.toLowerCase(); + langList.querySelectorAll('label').forEach(function (label) { + (label as HTMLElement).style.display = label.textContent + ?.toLowerCase() + .includes(searchTerm) + ? '' + : 'none'; + }); + }); - // Download PDF - if (downloadPdfBtn) { - downloadPdfBtn.addEventListener('click', function () { - if (pageState.searchablePdfBytes) { - downloadFile( - new Blob([new Uint8Array(pageState.searchablePdfBytes)], { type: 'application/pdf' }), - 'searchable.pdf' - ); - } + langList.addEventListener('change', function () { + const selected = Array.from( + langList.querySelectorAll('.lang-checkbox:checked') + ).map(function (cb) { + return tesseractLanguages[ + (cb as HTMLInputElement).value as keyof typeof tesseractLanguages + ]; + }); + + if (selectedLangsDisplay) { + selectedLangsDisplay.textContent = + selected.length > 0 ? selected.join(', ') : 'None'; + } + + if (processBtn) { + processBtn.disabled = selected.length === 0; + } + }); + } + + // Whitelist preset + if (presetSelect && whitelistInput) { + presetSelect.addEventListener('change', function () { + const preset = presetSelect.value; + if (preset && preset !== 'custom') { + whitelistInput.value = whitelistPresets[preset] || ''; + whitelistInput.disabled = true; + } else { + whitelistInput.disabled = false; + if (preset === '') { + whitelistInput.value = ''; + } + } + }); + } + + // Details toggle + document.querySelectorAll('details').forEach(function (details) { + details.addEventListener('toggle', function () { + const icon = details.querySelector('.details-icon') as HTMLElement; + if (icon) { + icon.style.transform = (details as HTMLDetailsElement).open + ? 'rotate(180deg)' + : 'rotate(0deg)'; + } + }); + }); + + // Process button + if (processBtn) { + processBtn.addEventListener('click', runOCR); + } + + // Copy button + if (copyBtn) { + copyBtn.addEventListener('click', function () { + const textOutput = document.getElementById( + 'ocr-text-output' + ) as HTMLTextAreaElement; + if (textOutput) { + navigator.clipboard.writeText(textOutput.value).then(function () { + copyBtn.innerHTML = + ''; + createIcons({ icons }); + + setTimeout(function () { + copyBtn.innerHTML = + ''; + createIcons({ icons }); + }, 2000); }); - } + } + }); + } + + // Download txt + if (downloadTxtBtn) { + downloadTxtBtn.addEventListener('click', function () { + const textOutput = document.getElementById( + 'ocr-text-output' + ) as HTMLTextAreaElement; + if (textOutput) { + const blob = new Blob([textOutput.value], { type: 'text/plain' }); + downloadFile(blob, 'ocr-text.txt'); + } + }); + } + + // Download PDF + if (downloadPdfBtn) { + downloadPdfBtn.addEventListener('click', function () { + if (pageState.searchablePdfBytes) { + downloadFile( + new Blob([new Uint8Array(pageState.searchablePdfBytes)], { + type: 'application/pdf', + }), + 'searchable.pdf' + ); + } + }); + } }); diff --git a/src/js/types/ocr-pdf-type.ts b/src/js/types/ocr-pdf-type.ts index b1ef7e401..00a340d91 100644 --- a/src/js/types/ocr-pdf-type.ts +++ b/src/js/types/ocr-pdf-type.ts @@ -1,10 +1,46 @@ export interface OcrWord { - text: string; - bbox: { x0: number; y0: number; x1: number; y1: number }; - confidence: number; + text: string; + bbox: { x0: number; y0: number; x1: number; y1: number }; + confidence: number; } export interface OcrState { - file: File | null; - searchablePdfBytes: Uint8Array | null; + file: File | null; + searchablePdfBytes: Uint8Array | null; +} + +export interface BBox { + x0: number; // left + y0: number; // top (in hOCR coordinate system, origin at top-left) + x1: number; // right + y1: number; // bottom +} + +export interface Baseline { + slope: number; + intercept: number; +} + +export interface OcrLine { + bbox: BBox; + baseline: Baseline; + textangle: number; + words: OcrWord[]; + direction: 'ltr' | 'rtl'; + injectWordBreaks: boolean; +} + +export interface OcrPage { + width: number; + height: number; + dpi: number; + lines: OcrLine[]; +} + +export interface WordTransform { + x: number; + y: number; + fontSize: number; + horizontalScale: number; + rotation: number; } diff --git a/src/js/utils/hocr-transform.ts b/src/js/utils/hocr-transform.ts new file mode 100644 index 000000000..eba2772ef --- /dev/null +++ b/src/js/utils/hocr-transform.ts @@ -0,0 +1,266 @@ +import { + BBox, + OcrLine, + OcrPage, + OcrWord, + WordTransform, + Baseline, +} from '@/types'; + +const BBOX_PATTERN = /bbox\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/; +const BASELINE_PATTERN = /baseline\s+([-+]?\d*\.?\d*)\s+([-+]?\d+)/; +const TEXTANGLE_PATTERN = /textangle\s+([-+]?\d*\.?\d*)/; + +export function parseBBox(title: string): BBox | null { + const match = title.match(BBOX_PATTERN); + if (!match) return null; + + return { + x0: parseInt(match[1], 10), + y0: parseInt(match[2], 10), + x1: parseInt(match[3], 10), + y1: parseInt(match[4], 10), + }; +} + +export function parseBaseline(title: string): Baseline { + const match = title.match(BASELINE_PATTERN); + if (!match) { + return { slope: 0, intercept: 0 }; + } + + return { + slope: parseFloat(match[1]) || 0, + intercept: parseInt(match[2], 10) || 0, + }; +} + +export function parseTextangle(title: string): number { + const match = title.match(TEXTANGLE_PATTERN); + if (!match) return 0; + return parseFloat(match[1]) || 0; +} + +export function getTextDirection(element: Element): 'ltr' | 'rtl' { + const dir = element.getAttribute('dir'); + return dir === 'rtl' ? 'rtl' : 'ltr'; +} + +export function shouldInjectWordBreaks(element: Element): boolean { + const lang = element.getAttribute('lang') || ''; + const cjkLangs = ['chi_sim', 'chi_tra', 'jpn', 'kor', 'zh', 'ja', 'ko']; + return !cjkLangs.includes(lang); +} + +export function normalizeText(text: string): string { + return text.normalize('NFKC'); +} + +export function parseHocrDocument(hocrText: string): OcrPage { + const parser = new DOMParser(); + const doc = parser.parseFromString(hocrText, 'text/html'); + + let width = 0; + let height = 0; + const pageDiv = doc.querySelector('.ocr_page'); + if (pageDiv) { + const title = pageDiv.getAttribute('title') || ''; + const bbox = parseBBox(title); + if (bbox) { + width = bbox.x1 - bbox.x0; + height = bbox.y1 - bbox.y0; + } + } + + const lines: OcrLine[] = []; + + const lineClasses = [ + 'ocr_line', + 'ocr_textfloat', + 'ocr_header', + 'ocr_caption', + ]; + const lineSelectors = lineClasses.map((c) => `.${c}`).join(', '); + const lineElements = doc.querySelectorAll(lineSelectors); + + if (lineElements.length > 0) { + lineElements.forEach((lineEl) => { + const line = parseHocrLine(lineEl); + if (line && line.words.length > 0) { + lines.push(line); + } + }); + } else { + const wordElements = doc.querySelectorAll('.ocrx_word'); + if (wordElements.length > 0) { + const words = parseWordsFromElements(wordElements); + if (words.length > 0) { + const allBBox = calculateBoundingBox(words.map((w) => w.bbox)); + lines.push({ + bbox: allBBox, + baseline: { slope: 0, intercept: 0 }, + textangle: 0, + words, + direction: 'ltr', + injectWordBreaks: true, + }); + } + } + } + + return { width, height, dpi: 72, lines }; +} + +function parseHocrLine(lineElement: Element): OcrLine | null { + const title = lineElement.getAttribute('title') || ''; + const bbox = parseBBox(title); + + if (!bbox) return null; + + const baseline = parseBaseline(title); + const textangle = parseTextangle(title); + + const parent = lineElement.closest('.ocr_par') || lineElement.parentElement; + const direction = parent ? getTextDirection(parent) : 'ltr'; + const injectWordBreaks = parent ? shouldInjectWordBreaks(parent) : true; + const wordElements = lineElement.querySelectorAll('.ocrx_word'); + const words = parseWordsFromElements(wordElements); + + return { + bbox, + baseline, + textangle, + words, + direction, + injectWordBreaks, + }; +} + +function parseWordsFromElements(wordElements: NodeListOf): OcrWord[] { + const words: OcrWord[] = []; + + wordElements.forEach((wordEl) => { + const title = wordEl.getAttribute('title') || ''; + const text = normalizeText((wordEl.textContent || '').trim()); + + if (!text) return; + + const bbox = parseBBox(title); + if (!bbox) return; + + const confMatch = title.match(/x_wconf\s+(\d+)/); + const confidence = confMatch ? parseInt(confMatch[1], 10) : 0; + + words.push({ + text, + bbox, + confidence, + }); + }); + + return words; +} + +function calculateBoundingBox(bboxes: BBox[]): BBox { + if (bboxes.length === 0) { + return { x0: 0, y0: 0, x1: 0, y1: 0 }; + } + + return { + x0: Math.min(...bboxes.map((b) => b.x0)), + y0: Math.min(...bboxes.map((b) => b.y0)), + x1: Math.max(...bboxes.map((b) => b.x1)), + y1: Math.max(...bboxes.map((b) => b.y1)), + }; +} + +/** + * Calculate the transformation parameters for drawing a word + * + * pdf-lib doesn't support horizontal text scaling (Tz operator), + * we calculate a font size that makes the text width exactly match the word bbox width. + * + * @param word - The word to position + * @param line - The line containing this word + * @param pageHeight - Height of the page in pixels (for coordinate flip) + * @param fontWidthFn - Function to calculate text width at a given font size + * @returns Transform parameters for pdf-lib + */ +export function calculateWordTransform( + word: OcrWord, + line: OcrLine, + pageHeight: number, + fontWidthFn: (text: string, fontSize: number) => number +): WordTransform { + const wordBBox = word.bbox; + const wordWidth = wordBBox.x1 - wordBBox.x0; + const wordHeight = wordBBox.y1 - wordBBox.y0; + + let fontSize = wordHeight; + const maxIterations = 10; + + for (let i = 0; i < maxIterations; i++) { + const currentWidth = fontWidthFn(word.text, fontSize); + if (currentWidth <= 0) break; + + const ratio = wordWidth / currentWidth; + const newFontSize = fontSize * ratio; + + if (Math.abs(newFontSize - fontSize) / fontSize < 0.01) { + fontSize = newFontSize; + break; + } + fontSize = newFontSize; + } + + fontSize = Math.max(1, Math.min(fontSize, wordHeight * 2)); + + const fontWidth = fontWidthFn(word.text, fontSize); + const horizontalScale = fontWidth > 0 ? wordWidth / fontWidth : 1; + + const slopeAngle = Math.atan(line.baseline.slope) * (180 / Math.PI); + const rotation = -line.textangle + slopeAngle; + + const x = wordBBox.x0; + + // pdf-lib draws text from baseline, so we position at word bottom + const y = pageHeight - wordBBox.y1; + + return { + x, + y, + fontSize, + horizontalScale, + rotation, + }; +} + +export function calculateSpaceTransform( + prevWord: OcrWord, + nextWord: OcrWord, + line: OcrLine, + pageHeight: number, + spaceWidthFn: (fontSize: number) => number +): { x: number; y: number; horizontalScale: number; fontSize: number } | null { + const lineHeight = line.bbox.y1 - line.bbox.y0; + const fontSize = Math.max(lineHeight + line.baseline.intercept, 1); + + const gapStart = prevWord.bbox.x1; + const gapEnd = nextWord.bbox.x0; + const gapWidth = gapEnd - gapStart; + + if (gapWidth <= 0) return null; + + const spaceWidth = spaceWidthFn(fontSize); + if (spaceWidth <= 0) return null; + + const horizontalScale = gapWidth / spaceWidth; + const baselineY = pageHeight - line.bbox.y1 - line.baseline.intercept; + + return { + x: gapStart, + y: baselineY, + horizontalScale, + fontSize, + }; +} From 1ac0f751e80ba26763f0fbf5609584ef8ba6f5c8 Mon Sep 17 00:00:00 2001 From: NightFeather Date: Sun, 11 Jan 2026 03:19:45 +0800 Subject: [PATCH 16/39] Add Traditional Chinese (`zh-TW`) localization --- TRANSLATION.md | 4 + nginx.conf | 2 +- public/locales/zh-TW/common.json | 318 +++++++++++++++++++++++++++++++ public/locales/zh-TW/tools.json | 282 +++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 12 +- vite.config.ts | 2 +- 6 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 public/locales/zh-TW/common.json create mode 100644 public/locales/zh-TW/tools.json diff --git a/TRANSLATION.md b/TRANSLATION.md index 57dc499bf..429f4ec5a 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -23,6 +23,8 @@ BentoPDF uses **i18next** for internationalization (i18n). Currently supported l - **German** (`de`) - **Vietnamese** (`vi`) - **Indonesian** (`id`) +- **Chinese** (`zh`) +- **Traditional Chinese (Taiwan)** (`zh-TW`) The app automatically detects the language from the URL path: @@ -30,6 +32,8 @@ The app automatically detects the language from the URL path: - `/de/` → German - `/vi/` → Vietnamese - `/id/` → Indonesian +- `/zh/` → Chinese +- `/zh-TW/` → Traditional Chinese (Taiwan) --- diff --git a/nginx.conf b/nginx.conf index a4904f55b..1d3f88434 100644 --- a/nginx.conf +++ b/nginx.conf @@ -26,7 +26,7 @@ http { root /usr/share/nginx/html; index index.html; - rewrite ^/(en|de|zh|vi|it|tr|id)/(.*)$ /$2 last; + rewrite ^/(en|de|zh|zh-TW|vi|it|tr|id)/(.*)$ /$2 last; location ~* \.html$ { expires 1h; diff --git a/public/locales/zh-TW/common.json b/public/locales/zh-TW/common.json new file mode 100644 index 000000000..cc7261a3a --- /dev/null +++ b/public/locales/zh-TW/common.json @@ -0,0 +1,318 @@ +{ + "nav": { + "home": "首頁", + "about": "關於我們", + "contact": "聯絡我們", + "licensing": "產品授權", + "allTools": "所有工具", + "openMainMenu": "開啟主選單", + "language": "語言" + }, + "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": "返回工具列表" + }, + "upload": { + "clickToSelect": "點擊以選擇檔案", + "orDragAndDrop": "或將檔案拖放到此處", + "pdfOrImages": "PDF 或圖片", + "filesNeverLeave": "你的檔案永遠不會離開你的裝置。", + "addMore": "添加更多檔案", + "clearAll": "清除全部" + }, + "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": "使用全螢幕寬度而非置中容器顯示所有工具", + "settingsAutoSaved": "設定會自動儲存", + "clickToSet": "點擊以設定", + "pressKeys": "按下按鍵...", + "warnings": { + "alreadyInUse": "快捷鍵已被占用", + "assignedTo": "已被指定為:", + "chooseDifferent": "請選擇一個不同的快捷鍵。", + "reserved": "保留快捷鍵警告", + "commonlyUsed": "常被用於:", + "unreliable": "這個快捷鍵可能與系統/瀏覽器行為衝突或無法穩定運作。", + "useAnyway": "仍要使用嗎?", + "resetTitle": "重設快捷鍵", + "resetMessage": "確定要將所有快捷鍵恢復為預設值嗎?

這個操作無法被撤回。", + "importSuccessTitle": "匯入成功", + "importSuccessMessage": "快捷鍵匯入成功!", + "importFailTitle": "匯入失敗", + "importFailMessage": "匯入快捷鍵失敗。無效的檔案格式。" + } + }, + "warning": { + "title": "警告", + "cancel": "取消", + "proceed": "繼續" + }, + "compliance": { + "title": "你的資料永遠不會離開你的裝置", + "weKeep": "我們確保", + "yourInfoSafe": "你的資訊安全", + "byFollowingStandards": "遵循全球安全標準。", + "processingLocal": "所有處理過程都在你的裝置上進行。", + "gdpr": { + "title": "符合 GDPR 規範", + "description": "保護歐盟境內個人的數據及隱私。" + }, + "ccpa": { + "title": "符合 CCPA 規範", + "description": "賦予加州居民對其個人資訊如何被蒐集、使用及分享的權利。" + }, + "hipaa": { + "title": "符合 HIPAA 規範", + "description": "制定處理美國健保系統中敏感健康資訊的規範。" + } + }, + "faq": { + "title": "常見", + "questions": "問題", + "isFree": { + "question": "BentoPDF 真的是免費的嗎?", + "answer": "沒錯,完全免費。BentoPDF 上的所有工具均為 100% 免費使用,並且沒有檔案限制、無須註冊且無浮水印。我們相信每個人都值得免費使用簡單且強大的 PDF 工具。" + }, + "areFilesSecure": { + "question": "我的檔案都是安全的嗎?它們都在哪裡被處理?", + "answer": "你的檔案都非常安全,因為它們從未離開你的電腦。所有處理過程都直接在你的網頁瀏覽器中進行(用戶端)。我們永遠不會將你的檔案上傳到伺服器,因此你對你的文件保有完全的隱私與控制權。" + }, + "platforms": { + "question": "我能在 Mac、Windows 和行動裝置上使用嗎?", + "answer": "可以!由於 BentoPDF 完全在你的瀏覽器中運作,它在任何有著現代網頁瀏覽器的系統中都能運行,包含 Windows、macOS、Linux、iOS 和 Android。" + }, + "gdprCompliant": { + "question": "BentoPDF 符合 GDPR 規範嗎?", + "answer": "是的。BentoPDF 完全符合 GDPR 規範。由於所有檔案處理都在你的瀏覽器本地發生且我們永不蒐集或傳輸你的檔案至任何伺服器,我們無法存取你的資料。這確保你的文件永遠都在你的控制之中。" + }, + "dataStorage": { + "question": "你會保存或追蹤我的檔案嗎?", + "answer": "不。我們永不儲存、追蹤或記錄你的檔案。你在 BentoPDF 上進行的任何操作都發生在你的瀏覽器記憶體中,並且會在你關閉頁面後立即消失。沒有上傳、沒有歷史紀錄且無伺服器參與。" + }, + "different": { + "question": "BentoPDF 跟其他的 PDF 工具有何不同之處?", + "answer": "大多數 PDF 工具都透過將你的檔案上傳至伺服器好進行處理。BentoPDF 永遠不會那麼做。我們使用安全且現代的網頁科技以在你的瀏覽器中直接處理檔案。這意味著更快的性能、更強的隱私與完全的安心。" + }, + "browserBased": { + "question": "瀏覽器端處理如何保障我的安全?", + "answer": "透過完全在你的瀏覽器內運作,BentoPDF 確保你的文件從未離開你的裝置。這消除了伺服器遭駭、資料外洩與未授權訪問的風險。你的檔案永遠都屬於你。" + }, + "analytics": { + "question": "你會使用 Cookies 或網站分析來追蹤我嗎?", + "answer": "我們在乎你的隱私。BentoPDF 並不追蹤個人資訊。我們僅使用 Simple Analytics 來查看匿名訪問次數。這代表我們能知道有多少使用者造訪過我們的網站,但我們永遠都不會知道你是誰。Simple Analytics 完全符合 GDPR 規範且尊重你的隱私。" + } + }, + "testimonials": { + "title": "看看我們的", + "users": "使用者", + "say": "怎麼說" + }, + "support": { + "title": "喜歡我的作品嗎?", + "description": "BentoPDF 是一個出於熱情開發的專案,旨在為每個人提供一個免費、注重隱私且強大的 PDF 工具組。如果有幫上你的忙,請考慮支持它的開發。每杯咖啡都意義重大!", + "buyMeCoffee": "買杯咖啡給我" + }, + "footer": { + "copyright": "© 2025 BentoPDF。版權所有。", + "version": "版本", + "company": "公司", + "aboutUs": "關於我們", + "faqLink": "常見問題", + "contactUs": "聯絡我們", + "legal": "法律", + "termsAndConditions": "服務條款", + "privacyPolicy": "隱私政策", + "followUs": "關注我們" + }, + "merge": { + "title": "合併 PDF", + "description": "合併整個檔案,或選擇特定頁面合併為新文件。", + "fileMode": "檔案模式", + "pageMode": "頁面模式", + "howItWorks": "使用說明:", + "fileModeInstructions": [ + "點擊並抓取圖標來改變檔案順序。", + "在每個文件的「頁碼」框中,你可以僅指定想要合併的頁面範圍(例如「1-3, 5」)。", + "將「頁碼」框留空以包含該檔案的所有頁面。" + ], + "pageModeInstructions": [ + "下列是你上傳的 PDF 中的所有頁面。", + "只要將個別頁面縮圖拖放到指定位置,即可為新檔案建立您想要的精確排序。" + ], + "mergePdfs": "合併 PDF" + }, + "common": { + "page": "頁", + "pages": "頁", + "of": " / ", + "download": "下載", + "cancel": "取消", + "save": "儲存", + "delete": "刪除", + "edit": "編輯", + "add": "添加", + "remove": "移除", + "loading": "載入中...", + "error": "錯誤", + "success": "成功", + "file": "檔案", + "files": "檔案" + }, + "about": { + "hero": { + "title": "我們相信 PDF 工具應該", + "subtitle": "快速、私密且免費。", + "noCompromises": "絕不妥協。" + }, + "mission": { + "title": "我們的任務", + "description": "在尊重你的隱私且從不要求收費的同時提供最全面的 PDF 工具箱。我們相信核心文件工具應讓任何人隨時隨地不受限的使用。" + }, + "philosophy": { + "label": "我們的核心理念", + "title": "永遠以隱私為重。", + "description": "在數據被商品化的時代,我們採取截然不同的做法。所有 BentoPDF 工具的處理流程皆在你的瀏覽器本地完成。這意味著你的檔案絕不觸及我們的伺服器,我們從未看見你的文件內容,更不會追蹤你的行為。你的文件將始終保持無可置疑的私密性。這不僅是功能,更是我們的立身之本。" + }, + "whyBentopdf": { + "title": "為何選擇", + "speed": { + "title": "生來迅捷", + "description": "無需等待與伺服器間的上傳和下載。透過在你的瀏覽器中使用 WebAssembly 等現代網路科技處理檔案,我們得以為所有工具提供無與倫比的速度。" + }, + "free": { + "title": "完全免費", + "description": "沒有試用期、訂閱、隱藏費用與所謂的「高級」功能。我們相信強大的 PDF 工具應該是一種公共設施,而非以營利為重。" + }, + "noAccount": { + "title": "無須帳號", + "description": "立即開始使用任何工具。我們不需要你的電子郵件、密碼或任何個人資訊。你的工作流程應當匿名且不受阻礙。" + }, + "openSource": { + "title": "開源精神", + "description": "將透明性視為核心打造。我們使用了如 PDF-lib 和 PDF.js 等優秀的開源庫,並且相信社群驅動力能讓強大的工具惠及每一個人。" + } + }, + "cta": { + "title": "準備好開始了嗎?", + "description": "加入成千上萬信任 BentoPDF 能勝任他們日常文件需求的使用者們。體驗隱私與性能所帶來的差距。", + "button": "探索所有工具" + } + }, + "contact": { + "title": "保持聯絡", + "subtitle": "我們很樂意收到你的訊息。無論你想提出的是問題、回饋或功能請求,都請隨時聯繫我們。", + "email": "你可以直接透過電子郵件聯繫我們:" + }, + "licensing": { + "title": "授權使用", + "subtitle": "選擇適合需求的產品授權。" + }, + "multiTool": { + "uploadPdfs": "上傳 PDF", + "upload": "上傳", + "addBlankPage": "添加空白頁面", + "edit": "編輯:", + "undo": "復原", + "redo": "取消復原", + "reset": "重設", + "selection": "選取:", + "selectAll": "選取全部", + "deselectAll": "取消選取全部", + "rotate": "旋轉:", + "rotateLeft": "左", + "rotateRight": "右", + "transform": "變換:", + "duplicate": "複製", + "split": "分割", + "clear": "清除:", + "delete": "刪除", + "download": "下載:", + "downloadSelected": "下載選取的項目", + "exportPdf": "匯出 PDF", + "uploadPdfFiles": "選擇 PDF 檔案", + "dragAndDrop": "拖放 PDF 檔案至此處,或是點擊以選取", + "selectFiles": "選擇檔案", + "renderingPages": "渲染頁面...", + "actions": { + "duplicatePage": "複製此頁", + "deletePage": "刪除此頁", + "insertPdf": "在此頁後插入 PDF", + "toggleSplit": "在此頁後切換分割" + }, + "pleaseWait": "請稍後", + "pagesRendering": "正在渲染頁面。請稍後...", + "noPagesSelected": "未選擇頁面", + "selectOnePage": "請至少選擇一頁以開始下載。", + "noPages": "無頁面", + "noPagesToExport": "無可匯出的頁面。", + "renderingTitle": "正在渲染頁面預覽", + "errorRendering": "無法渲染頁面縮圖", + "error": "錯誤", + "failedToLoad": "載入失敗" + } +} \ No newline at end of file diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json new file mode 100644 index 000000000..1a296ed88 --- /dev/null +++ b/public/locales/zh-TW/tools.json @@ -0,0 +1,282 @@ +{ + "categories": { + "popularTools": "熱門工具", + "editAnnotate": "編輯與註解", + "convertToPdf": "轉換為 PDF", + "convertFromPdf": "從 PDF 轉換", + "organizeManage": "組織與管理", + "optimizeRepair": "優化與修復", + "securePdf": "安全 PDF" + }, + "pdfMultiTool": { + "name": "PDF 多功能工具", + "subtitle": "在統一的頁面中合併、分割、組織、刪除、旋轉、添加空白頁面、提取與複製。" + }, + "mergePdf": { + "name": "合併 PDF", + "subtitle": "將多個 PDF 合併為一個檔案。保留書籤。" + }, + "splitPdf": { + "name": "分割 PDF", + "subtitle": "將指定範圍的頁面提取為新的 PDF。" + }, + "compressPdf": { + "name": "壓縮 PDF", + "subtitle": "降低你的 PDF 檔案大小。" + }, + "pdfEditor": { + "name": "PDF 編輯器", + "subtitle": "註解、螢光、塗黑、評論、添加圖形或圖片、搜尋與查看 PDF。" + }, + "jpgToPdf": { + "name": "JPG 轉 PDF", + "subtitle": "從一張或多張 JPG 圖片建立 PDF。" + }, + "signPdf": { + "name": "簽署 PDF", + "subtitle": "繪製、輸入或上傳你的簽名。" + }, + "cropPdf": { + "name": "裁切 PDF", + "subtitle": "修剪你的 PDF 中所有頁面的邊界。" + }, + "extractPages": { + "name": "提取頁面", + "subtitle": "將選取的頁面保存為新的檔案。" + }, + "duplicateOrganize": { + "name": "複製與組織", + "subtitle": "複製、重新排序與刪除頁面。" + }, + "deletePages": { + "name": "刪除頁面", + "subtitle": "移除你的文件中的特定頁面。" + }, + "editBookmarks": { + "name": "編輯書籤", + "subtitle": "添加、編輯、匯入、刪除與提取 PDF 書籤。" + }, + "tableOfContents": { + "name": "目錄", + "subtitle": "從 PDF 書籤生成目錄頁。" + }, + "pageNumbers": { + "name": "頁碼", + "subtitle": "在你的文件中插入頁碼。" + }, + "addWatermark": { + "name": "添加浮水印", + "subtitle": "在你的 PDF 頁面上壓印文字或圖片。" + }, + "headerFooter": { + "name": "頁首與頁尾", + "subtitle": "在頁面的頂部與底部新增文字。" + }, + "invertColors": { + "name": "反轉顏色", + "subtitle": "為你的 PDF 建立深色版本。" + }, + "backgroundColor": { + "name": "背景顏色", + "subtitle": "更改你的 PDF 的背景顏色。" + }, + "changeTextColor": { + "name": "更改文字顏色", + "subtitle": "更改你的 PDF 中的文字顏色。" + }, + "addStamps": { + "name": "添加印章", + "subtitle": "使用註解工具列在你的 PDF 中添加圖片印章。", + "usernameLabel": "印章使用者名稱", + "usernamePlaceholder": "輸入你的名稱(印章用)", + "usernameHint": "該名稱會出現在你建立的印章上。" + }, + "removeAnnotations": { + "name": "移除註解", + "subtitle": "去除留言、螢光與連結。" + }, + "pdfFormFiller": { + "name": "PDF 表單填寫器", + "subtitle": "直接在你的瀏覽器中填寫表單。支援 XFA 表單。" + }, + "createPdfForm": { + "name": "建立 PDF 表單", + "subtitle": "透過拖放文字框建立可填寫的 PDF 表單。" + }, + "removeBlankPages": { + "name": "移除空白頁面", + "subtitle": "自動偵測並刪除空白頁面。" + }, + "imageToPdf": { + "name": "圖片轉 PDF", + "subtitle": "將 JPG、PNG、WebP、BMP、TIFF、SVG 與 HEIC 轉換為 PDF。" + }, + "pngToPdf": { + "name": "PNG 轉 PDF", + "subtitle": "從一張或多張 PNG 圖片建立 PDF。" + }, + "webpToPdf": { + "name": "WebP 轉 PDF", + "subtitle": "從一張或多張 WebP 圖片建立 PDF。" + }, + "svgToPdf": { + "name": "SVG 轉 PDF", + "subtitle": "從一張或多張 SVG 圖片建立 PDF。" + }, + "bmpToPdf": { + "name": "BMP 轉 PDF", + "subtitle": "從一張或多張 BMP 圖片建立 PDF。" + }, + "heicToPdf": { + "name": "HEIC 轉 PDF", + "subtitle": "從一張或多張 HEIC 圖片建立 PDF。" + }, + "tiffToPdf": { + "name": "TIFF 轉 PDF", + "subtitle": "從一張或多張 TIFF 圖片建立 PDF。" + }, + "textToPdf": { + "name": "Text 轉 PDF", + "subtitle": "將純文字檔案轉換為 PDF。" + }, + "jsonToPdf": { + "name": "JSON 轉 PDF", + "subtitle": "將 JSON 檔案轉換為 PDF 格式。" + }, + "pdfToJpg": { + "name": "PDF 轉 JPG", + "subtitle": "將每個 PDF 頁面轉換為 JPG 圖片。" + }, + "pdfToPng": { + "name": "PDF 轉 PNG", + "subtitle": "將每個 PDF 頁面轉換為 PNG 圖片。" + }, + "pdfToWebp": { + "name": "PDF 轉 WebP", + "subtitle": "將每個 PDF 頁面轉換為 WebP 圖片。" + }, + "pdfToBmp": { + "name": "PDF 轉 BMP", + "subtitle": "將每個 PDF 頁面轉換為 BMP 圖片。" + }, + "pdfToTiff": { + "name": "PDF 轉 TIFF", + "subtitle": "將每個 PDF 頁面轉換為 TIFF 圖片。" + }, + "pdfToGreyscale": { + "name": "PDF 轉灰階", + "subtitle": "將所有顏色轉換為黑白。" + }, + "pdfToJson": { + "name": "PDF 轉 JSON", + "subtitle": "將 PDF 檔案轉換為 JSON 格式。" + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "使 PDF 可搜尋且可複製。" + }, + "alternateMix": { + "name": "交錯混合頁面", + "subtitle": "將每個 PDF 的頁面交錯合併。保留書籤。" + }, + "addAttachments": { + "name": "添加附件", + "subtitle": "嵌入一個或多個檔案至你的 PDF 中。" + }, + "extractAttachments": { + "name": "提取附件", + "subtitle": "從 PDF 中提取所有嵌入的檔案為 ZIP。" + }, + "editAttachments": { + "name": "編輯附件", + "subtitle": "查看或移除你的 PDF 中的附件。" + }, + "dividePages": { + "name": "分割頁面", + "subtitle": "垂直或水平分割頁面。" + }, + "addBlankPage": { + "name": "添加空白頁面", + "subtitle": "在你的 PDF 中的任一位置插入空白頁面。" + }, + "reversePages": { + "name": "反轉頁面", + "subtitle": "反轉你的文件中所有頁面的順序。" + }, + "rotatePdf": { + "name": "旋轉 PDF", + "subtitle": "以 90 度增量旋轉頁面。" + }, + "nUpPdf": { + "name": "N-Up PDF", + "subtitle": "將多個頁面排列在單張紙上。" + }, + "combineToSinglePage": { + "name": "合併為單一頁面", + "subtitle": "將所有頁面縫合為一個單一且連續的滾動頁面。" + }, + "viewMetadata": { + "name": "查看元資料", + "subtitle": "檢視你的 PDF 中的隱藏屬性。" + }, + "editMetadata": { + "name": "編輯元資料", + "subtitle": "更改作者、標題和其他屬性。" + }, + "pdfsToZip": { + "name": "PDF 轉 ZIP", + "subtitle": "將多個 PDF 檔案打包為 ZIP 壓縮檔。" + }, + "comparePdfs": { + "name": "比較 PDF", + "subtitle": "並排比較兩個 PDF。" + }, + "posterizePdf": { + "name": "海報化 PDF", + "subtitle": "將大頁面分割為多個較小的頁面。" + }, + "fixPageSize": { + "name": "修復頁面大小", + "subtitle": "將所有頁面標準化為統一尺寸。" + }, + "linearizePdf": { + "name": "線性化 PDF", + "subtitle": "為快速網頁瀏覽優化 PDF。" + }, + "pageDimensions": { + "name": "頁面尺寸", + "subtitle": "分析頁面大小、方向和單位。" + }, + "removeRestrictions": { + "name": "移除限制", + "subtitle": "移除與數位簽名的 PDF 檔案相關的密碼保護與安全限制。" + }, + "repairPdf": { + "name": "修復 PDF", + "subtitle": "從受損的 PDF 檔案中復原資料。" + }, + "encryptPdf": { + "name": "加密 PDF", + "subtitle": "透過添加密碼為你的 PDF 上鎖。" + }, + "sanitizePdf": { + "name": "清理 PDF", + "subtitle": "移除元資料、註解、腳本與其他資料。" + }, + "decryptPdf": { + "name": "解密 PDF", + "subtitle": "透過移除密碼保護解鎖 PDF。" + }, + "flattenPdf": { + "name": "平面化 PDF", + "subtitle": "使表單欄位和註解不可編輯。" + }, + "removeMetadata": { + "name": "移除元資料", + "subtitle": "除去你的 PDF 中的隱藏資料。" + }, + "changePermissions": { + "name": "更改權限", + "subtitle": "設定或變更 PDF 上的使用者權限。" + } +} \ No newline at end of file diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index ed104643d..ea191b847 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -7,6 +7,7 @@ export const supportedLanguages = [ 'en', 'de', 'zh', + 'zh-TW', 'vi', 'tr', 'id', @@ -18,6 +19,7 @@ export const languageNames: Record = { en: 'English', de: 'Deutsch', zh: '中文', + "zh-TW": '繁體中文(台灣)', vi: 'Tiếng Việt', tr: 'Türkçe', id: 'Bahasa Indonesia', @@ -26,7 +28,7 @@ export const languageNames: Record = { export const getLanguageFromUrl = (): SupportedLanguage => { const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi|tr|id|it)(?:\/|$)/); + const langMatch = path.match(/^\/(en|de|zh|zh-TW|vi|tr|id|it)(?:\/|$)/); if ( langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage) @@ -88,9 +90,9 @@ export const changeLanguage = (lang: SupportedLanguage): void => { const currentLang = getLanguageFromUrl(); let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { - newPath = currentPath.replace(/^\/(en|de|zh|vi|tr|id|it)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)$/)) { + if (currentPath.match(/^\/(en|de|zh|zh-TW|vi|tr|id|it)\//)) { + newPath = currentPath.replace(/^\/(en|de|zh|zh-TW|vi|tr|id|it)\//, `/${lang}/`); + } else if (currentPath.match(/^\/(en|de|zh|zh-TW|vi|tr|id|it)$/)) { newPath = `/${lang}`; } else { newPath = `/${lang}${currentPath}`; @@ -154,7 +156,7 @@ export const rewriteLinks = (): void => { return; } - if (href.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { + if (href.match(/^\/(en|de|zh|zh-TW|vi|tr|id|it)\//)) { return; } let newHref: string; diff --git a/vite.config.ts b/vite.config.ts index c4caa121a..e6f2cdb0e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ function pagesRewritePlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url?.split('?')[0] || ''; - const langMatch = url.match(/^\/(en|de|zh|vi|it|id|tr)(\/.*)?$/); + const langMatch = url.match(/^\/(en|de|zh|zh-TW|vi|it|id|tr)(\/.*)?$/); if (langMatch) { const lang = langMatch[1]; const restOfPath = langMatch[2] || '/'; From 3137b0e2c4aaffbcad1e79cdfc8939bcc460c5c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:30:32 +0000 Subject: [PATCH 17/39] @NightFeather0615 has signed the CLA in alam00000/bentopdf#379 --- signatures/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/signatures/cla.json b/signatures/cla.json index 42dcea152..8327ee111 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -239,6 +239,14 @@ "created_at": "2026-01-08T08:29:28Z", "repoId": 1074785178, "pullRequestNo": 289 + }, + { + "name": "NightFeather0615", + "id": 77222233, + "comment_id": 3733421577, + "created_at": "2026-01-10T19:30:21Z", + "repoId": 1074785178, + "pullRequestNo": 379 } ] } \ No newline at end of file From d7fce5987dca5be7a2f60c99e6968e1ef5abbe29 Mon Sep 17 00:00:00 2001 From: gianlucaalfa Date: Sun, 11 Jan 2026 11:07:13 +0100 Subject: [PATCH 18/39] fix-italian-translation --- public/locales/it/common.json | 180 +++++++++++++++++----------------- public/locales/it/tools.json | 39 ++++++-- 2 files changed, 123 insertions(+), 96 deletions(-) diff --git a/public/locales/it/common.json b/public/locales/it/common.json index c620102d8..2b7258202 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -1,50 +1,50 @@ { "nav": { "home": "Home", - "about": "Chi Siamo", + "about": "Chi siamo", "contact": "Contatti", "licensing": "Licenze", - "allTools": "Tutti gli Strumenti", - "openMainMenu": "Apri il Menu Principale", + "allTools": "Tutti gli strumenti", + "openMainMenu": "Apri il menu principale", "language": "Lingua" }, "donation": { - "message": "Love BentoPDF? Help us keep it free and open source!", - "button": "Donate" + "message": "Ti piace BentoPDF? Aiutaci a mantenerlo gratuito e open source!", + "button": "Dona" }, "hero": { - "title": "I", - "pdfToolkit": "tuoi attrezzi per i PDF", - "builtForPrivacy": "creati per la privacy", - "noSignups": "Nessun iscrizione", + "title": "Il", + "pdfToolkit": "kit di strumenti PDF", + "builtForPrivacy": "pensato per la privacy", + "noSignups": "Nessuna registrazione", "unlimitedUse": "Uso illimitato", "worksOffline": "Funziona offline", - "startUsing": "Inizia ad usarlo ora" + "startUsing": "Inizia a usarlo ora" }, "usedBy": { "title": "Usato da aziende e persone che lavorano in" }, "features": { - "title": "Perchè scegliere", + "title": "Perché scegliere", "bentoPdf": "BentoPDF?", "noSignup": { - "title": "Nessuna Registrazione", - "description": "Usa subito, senza account o email." + "title": "Nessuna registrazione", + "description": "Inizia subito, senza account né email." }, "noUploads": { "title": "Nessun caricamento", "description": "100% client-side, i tuoi file non lasciano mai il dispositivo." }, "foreverFree": { - "title": "Sempre Gratis", + "title": "Sempre gratis", "description": "Tutti gli strumenti, nessuna prova, nessun paywall." }, "noLimits": { - "title": "Senza Limiti", + "title": "Senza limiti", "description": "Usalo quanto vuoi, senza limiti nascosti." }, "batchProcessing": { - "title": "Elaborazione in Batch", + "title": "Elaborazione in batch", "description": "Gestisci un numero illimitato di PDF in un'unica operazione." }, "lightningFast": { @@ -55,15 +55,15 @@ "tools": { "title": "Inizia con", "toolsLabel": "Strumenti", - "subtitle": "Clicca uno strumento per aprire il caricatore di file", - "searchPlaceholder": "Cerca uno strumento (es. 'split', 'organize'...)", - "backToTools": "Torna agli Strumenti", - "firstLoadNotice": "Il primo caricamento richiede un momento mentre scarichiamo il nostro motore di conversione. Dopo di ciò, tutti i caricamenti saranno immediati." + "subtitle": "Clicca su uno strumento per aprire il caricatore di file", + "searchPlaceholder": "Cerca uno strumento (es. \"split\", \"organizza\"...)", + "backToTools": "Torna agli strumenti", + "firstLoadNotice": "Il primo caricamento richiede qualche istante mentre scarichiamo il motore di conversione. Dopo di ciò, tutti i caricamenti saranno immediati." }, "upload": { "clickToSelect": "Clicca per selezionare un file", - "orDragAndDrop": "o trascina e rilascia", - "pdfOrImages": "PDF o Immagini", + "orDragAndDrop": "oppure trascina e rilascia", + "pdfOrImages": "PDF o immagini", "filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.", "addMore": "Aggiungi altri file", "clearAll": "Svuota tutto" @@ -76,7 +76,7 @@ "ok": "OK" }, "preview": { - "title": "Anteprima Documento", + "title": "Anteprima documento", "downloadAsPdf": "Scarica come PDF", "close": "Chiudi" }, @@ -87,7 +87,7 @@ "displayPreferences": "Preferenze di visualizzazione", "searchShortcuts": "Cerca scorciatoie...", "shortcutsInfo": "Premi e tieni premuti i tasti per impostare una scorciatoia. Le modifiche vengono salvate automaticamente.", - "shortcutsWarning": "⚠️ Evita scorciatoie comuni del browser (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N ecc.) poiché potrebbero non funzionare in modo affidabile.", + "shortcutsWarning": "⚠️ Evita le scorciatoie comuni del browser (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N ecc.) perché potrebbero non funzionare in modo affidabile.", "import": "Importa", "export": "Esporta", "resetToDefaults": "Reimposta ai valori predefiniti", @@ -95,7 +95,7 @@ "fullWidthDescription": "Usa l'intera larghezza dello schermo per tutti gli strumenti invece di un contenitore centrato", "settingsAutoSaved": "Le impostazioni vengono salvate automaticamente", "clickToSet": "Clicca per impostare", - "pressKeys": "Premi tasti...", + "pressKeys": "Premi i tasti...", "warnings": { "alreadyInUse": "Scorciatoia già in uso", "assignedTo": "è già assegnata a:", @@ -104,11 +104,11 @@ "commonlyUsed": "è comunemente usata per:", "unreliable": "Questa scorciatoia potrebbe non funzionare in modo affidabile o potrebbe avere conflitti con il comportamento del browser/sistema.", "useAnyway": "Vuoi usarla comunque?", - "resetTitle": "Reimposta Scorciatoie", + "resetTitle": "Reimposta scorciatoie", "resetMessage": "Sei sicuro di voler reimpostare tutte le scorciatoie ai valori predefiniti?

Questa azione non può essere annullata.", - "importSuccessTitle": "Importazione Riuscita", + "importSuccessTitle": "Importazione riuscita", "importSuccessMessage": "Scorciatoie importate con successo!", - "importFailTitle": "Importazione Fallita", + "importFailTitle": "Importazione fallita", "importFailMessage": "Impossibile importare le scorciatoie. Formato file non valido." } }, @@ -137,8 +137,8 @@ } }, "faq": { - "title": "Domande Frequenti", - "questions": "Domande", + "title": "Domande", + "questions": "Frequenti", "isFree": { "question": "BentoPDF è davvero gratuito?", "answer": "Sì, assolutamente. Tutti gli strumenti su BentoPDF sono gratuiti al 100%, senza limiti di file, senza registrazioni e senza filigrane. Crediamo che tutti debbano poter accedere a strumenti PDF semplici e potenti senza barriere a pagamento." @@ -174,8 +174,8 @@ }, "testimonials": { "title": "Cosa", - "users": "i Nostri Utenti", - "say": "Dicono" + "users": "dicono i nostri", + "say": "utenti" }, "support": { "title": "Ti piace il mio lavoro?", @@ -186,19 +186,19 @@ "copyright": "© 2025 BentoPDF. Tutti i diritti riservati.", "version": "Versione", "company": "Azienda", - "aboutUs": "Chi Siamo", + "aboutUs": "Chi siamo", "faqLink": "FAQ", "contactUs": "Contattaci", "legal": "Legale", - "termsAndConditions": "Termini e Condizioni", - "privacyPolicy": "Informativa sulla Privacy", + "termsAndConditions": "Termini e condizioni", + "privacyPolicy": "Informativa sulla privacy", "followUs": "Seguici" }, "merge": { "title": "Unisci PDF", "description": "Combina file interi, oppure seleziona pagine specifiche da unire in un nuovo documento.", - "fileMode": "Modalità File", - "pageMode": "Modalità Pagina", + "fileMode": "Modalità file", + "pageMode": "Modalità pagina", "howItWorks": "Come funziona:", "fileModeInstructions": [ "Clicca e trascina l'icona per cambiare l'ordine dei file.", @@ -247,7 +247,7 @@ "title": "Perché BentoPDF", "speed": { "title": "Progettato per la velocità", - "description": "Nessuna attesa per upload o download verso un server. Elaborando i file direttamente nel browser con tecnologie web moderne come WebAssembly, offriamo velocità impareggiabile per tutti i nostri strumenti." + "description": "Nessuna attesa per upload o download verso un server. Elaborando i file direttamente nel browser con tecnologie web moderne come WebAssembly, offriamo una velocità impareggiabile per tutti i nostri strumenti." }, "free": { "title": "Completamente gratuito", @@ -266,58 +266,58 @@ "title": "Pronto per iniziare?", "description": "Unisciti a migliaia di utenti che si affidano a BentoPDF per le loro esigenze quotidiane sui documenti. Sperimenta la differenza che privacy e prestazioni possono offrire.", "button": "Esplora tutti gli strumenti" - }, - "contact": { - "title": "Contattaci", - "subtitle": "Ci farebbe piacere sentirti. Che tu abbia una domanda, un feedback o una richiesta di funzionalità, non esitare a contattarci.", - "email": "Puoi contattarci direttamente via email a:" - }, - "licensing": { - "title": "Licenze per", - "subtitle": "Scegli la licenza che si adatta alle tue esigenze." - }, - "multiTool": { - "uploadPdfs": "Carica PDF", - "upload": "Carica", - "addBlankPage": "Aggiungi pagina vuota", - "edit": "Modifica:", - "undo": "Annulla", - "redo": "Ripeti", - "reset": "Reimposta", - "selection": "Selezione:", - "selectAll": "Seleziona tutto", - "deselectAll": "Deseleziona tutto", - "rotate": "Ruota:", - "rotateLeft": "Sinistra", - "rotateRight": "Destra", - "transform": "Trasforma:", - "duplicate": "Duplica", - "split": "Dividi", - "clear": "Svuota:", - "delete": "Elimina", - "download": "Scarica:", - "downloadSelected": "Scarica selezionati", - "exportPdf": "Esporta PDF", - "uploadPdfFiles": "Seleziona file PDF", - "dragAndDrop": "Trascina e rilascia i file PDF qui, oppure clicca per selezionare", - "selectFiles": "Seleziona file", - "renderingPages": "Generazione anteprime pagine...", - "actions": { - "duplicatePage": "Duplica questa pagina", - "deletePage": "Elimina questa pagina", - "insertPdf": "Inserisci PDF dopo questa pagina", - "toggleSplit": "Attiva la divisione dopo questa pagina" - }, - "pleaseWait": "Attendere...", - "pagesRendering": "Le pagine sono ancora in fase di generazione. Attendere...", - "noPagesSelected": "Nessuna pagina selezionata", - "selectOnePage": "Seleziona almeno una pagina da scaricare.", - "noPages": "Nessuna pagina", - "noPagesToExport": "Non ci sono pagine da esportare.", - "renderingTitle": "Generazione anteprime delle pagine", - "errorRendering": "Impossibile generare le miniature delle pagine", - "error": "Errore", - "failedToLoad": "Caricamento fallito" } + }, + "contact": { + "title": "Contattaci", + "subtitle": "Ci farebbe piacere sentirti. Che tu abbia una domanda, un feedback o una richiesta di funzionalità, non esitare a contattarci.", + "email": "Puoi contattarci direttamente via email a:" + }, + "licensing": { + "title": "Licenze per", + "subtitle": "Scegli la licenza che si adatta alle tue esigenze." + }, + "multiTool": { + "uploadPdfs": "Carica PDF", + "upload": "Carica", + "addBlankPage": "Aggiungi pagina vuota", + "edit": "Modifica:", + "undo": "Annulla", + "redo": "Ripeti", + "reset": "Reimposta", + "selection": "Selezione:", + "selectAll": "Seleziona tutto", + "deselectAll": "Deseleziona tutto", + "rotate": "Ruota:", + "rotateLeft": "Sinistra", + "rotateRight": "Destra", + "transform": "Trasforma:", + "duplicate": "Duplica", + "split": "Dividi", + "clear": "Svuota:", + "delete": "Elimina", + "download": "Scarica:", + "downloadSelected": "Scarica selezionati", + "exportPdf": "Esporta PDF", + "uploadPdfFiles": "Seleziona file PDF", + "dragAndDrop": "Trascina e rilascia i file PDF qui, oppure clicca per selezionare", + "selectFiles": "Seleziona file", + "renderingPages": "Generazione anteprime pagine...", + "actions": { + "duplicatePage": "Duplica questa pagina", + "deletePage": "Elimina questa pagina", + "insertPdf": "Inserisci PDF dopo questa pagina", + "toggleSplit": "Attiva la divisione dopo questa pagina" + }, + "pleaseWait": "Attendere...", + "pagesRendering": "Le pagine sono ancora in fase di generazione. Attendere...", + "noPagesSelected": "Nessuna pagina selezionata", + "selectOnePage": "Seleziona almeno una pagina da scaricare.", + "noPages": "Nessuna pagina", + "noPagesToExport": "Non ci sono pagine da esportare.", + "renderingTitle": "Generazione anteprime delle pagine", + "errorRendering": "Impossibile generare le miniature delle pagine", + "error": "Errore", + "failedToLoad": "Caricamento fallito" } } diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 9992060a7..0caf98e36 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -3,18 +3,18 @@ "popularTools": "Strumenti popolari", "editAnnotate": "Modifica e Annota", "convertToPdf": "Converti in PDF", - "convertFromPdf": "Convert da PDF", + "convertFromPdf": "Converti da PDF", "organizeManage": "Organizza e Gestisci", "optimizeRepair": "Ottimizza e Ripara", "securePdf": "Proteggi PDF" }, "pdfMultiTool": { - "name": "PDF Multi Tool", - "subtitle": "Unisci, Dividi, Organizza, Elimina, Ruota, Aggiungi Pagine Vuote, Estrai e Duplica in un'interfaccia unificata." + "name": "Strumento PDF multifunzione", + "subtitle": "Unisci, dividi, organizza, elimina, ruota, aggiungi pagine vuote, estrai e duplica in un'interfaccia unificata." }, "mergePdf": { "name": "Unisci PDF", - "subtitle": "Unisci più PDF in un unico file. Conserva i Segnalibri." + "subtitle": "Unisci più PDF in un unico file. Conserva i segnalibri." }, "splitPdf": { "name": "Dividi PDF", @@ -68,7 +68,7 @@ }, "duplicateOrganize": { "name": "Duplica e Organizza", - "subtitle": "Duplica, riordina e elimina pagine." + "subtitle": "Duplica, riordina ed elimina pagine." }, "deletePages": { "name": "Elimina Pagine", @@ -199,7 +199,7 @@ }, "alternateMix": { "name": "Alterna e Riordina Pagine", - "subtitle": "Unisci PDF sostituendo le pagine di ogni file. Conserva i segnalibri." + "subtitle": "Unisci i PDF alternando le pagine di ogni file. Conserva i segnalibri." }, "addAttachments": { "name": "Aggiungi Allegati", @@ -489,6 +489,33 @@ "note": "Questo strumento funziona SOLO con PDF creati digitalmente. Per documenti scansionati o basati su immagini, usa invece il nostro strumento OCR PDF.", "convertButton": "Estrai testo" }, + "digitalSignPdf": { + "name": "Firma digitale PDF", + "pageTitle": "Firma digitale PDF - Aggiungi firma crittografica | BentoPDF", + "subtitle": "Aggiungi una firma digitale crittografica al tuo PDF usando certificati X.509. Supporta i formati PKCS#12 (.pfx, .p12) e PEM. La tua chiave privata non lascia mai il browser.", + "certificateSection": "Certificato", + "uploadCert": "Carica certificato (.pfx, .p12)", + "certPassword": "Password del certificato", + "certPasswordPlaceholder": "Inserisci la password del certificato", + "certInfo": "Informazioni sul certificato", + "certSubject": "Soggetto", + "certIssuer": "Emittente", + "certValidity": "Valido", + "signatureDetails": "Dettagli della firma (opzionale)", + "reason": "Motivo", + "reasonPlaceholder": "es. Approvo questo documento", + "location": "Luogo", + "locationPlaceholder": "es. Roma, Italia", + "contactInfo": "Contatto", + "contactPlaceholder": "es. email@example.com", + "applySignature": "Applica firma digitale", + "successMessage": "PDF firmato con successo! La firma può essere verificata in qualsiasi lettore PDF." + }, + "validateSignaturePdf": { + "name": "Verifica firma PDF", + "pageTitle": "Verifica firma PDF - Controlla firme digitali | BentoPDF", + "subtitle": "Verifica le firme digitali nei tuoi PDF. Controlla la validità del certificato, visualizza i dati del firmatario e conferma l'integrità del documento. Tutto avviene nel tuo browser." + }, "emailToPdf": { "name": "Email in PDF", "subtitle": "Converti file email (EML, MSG) in formato PDF. Supporta esportazioni Outlook e formati email standard.", From 41970ec330018ba8204f9820e112074ba2451550 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:30:57 +0000 Subject: [PATCH 19/39] @gianlucaalfa has signed the CLA in alam00000/bentopdf#380 --- signatures/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/signatures/cla.json b/signatures/cla.json index 8327ee111..c85ba3b5d 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -247,6 +247,14 @@ "created_at": "2026-01-10T19:30:21Z", "repoId": 1074785178, "pullRequestNo": 379 + }, + { + "name": "gianlucaalfa", + "id": 10059028, + "comment_id": 3734373463, + "created_at": "2026-01-11T10:30:42Z", + "repoId": 1074785178, + "pullRequestNo": 380 } ] } \ No newline at end of file From 584acc68d06bce737969229236332125c775b14a Mon Sep 17 00:00:00 2001 From: Stanislas MEZUREUX Date: Sun, 14 Dec 2025 00:20:40 +0100 Subject: [PATCH 20/39] =?UTF-8?q?feat(i18n):=20Add=20French=20(Fran=C3=A7a?= =?UTF-8?q?is)=20language=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 187 ++++++++++---------- TRANSLATION.md | 38 ++-- nginx.conf | 2 +- public/locales/fr/common.json | 323 ++++++++++++++++++++++++++++++++++ public/locales/fr/tools.json | 282 +++++++++++++++++++++++++++++ src/js/i18n/i18n.ts | 12 +- src/pages/encrypt-pdf.html | 2 +- src/pages/form-creator.html | 1 - vite.config.ts | 2 +- 9 files changed, 733 insertions(+), 116 deletions(-) create mode 100644 public/locales/fr/common.json create mode 100644 public/locales/fr/tools.json diff --git a/README.md b/README.md index 379c25f3c..534a32592 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Have questions, feature requests, or want to chat with the community? Join our D [![Documentation](https://img.shields.io/badge/Docs-VitePress-646cff?style=for-the-badge&logo=vite&logoColor=white)](https://bentopdf.com/docs/) Visit our [Documentation](https://bentopdf.com/docs/) for: + - **Getting Started** guide - **Tools Reference** (50+ tools) - **Self-Hosting** guides (Docker, Vercel, Netlify, Cloudflare, AWS, Hostinger, Nginx, Apache) @@ -75,68 +76,68 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs. ### Organize & Manage PDFs -| Tool Name | Description | -| :------------------------ | :------------------------------------------------------------------------- | -| **Merge PDFs** | Combine multiple PDF files into one. | -| **Split PDFs** | Extract specific pages or divide a document into smaller files. | -| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. | -| **Extract Pages** | Save a specific range of pages as a new PDF. | -| **Delete Pages** | Remove unwanted pages from your document. | -| **Rotate PDF** | Rotate individual or all pages in a document. | -| **N-Up PDF** | Combine multiple pages onto a single page. | -| **View PDF** | A powerful, integrated PDF viewer. | -| **Alternate & Mix pages** | Merge pages by alternating pages from each PDF. | -| **Posterize PDF** | Split a PDF into multiple smaller pages for print. | -| **PDF Multi Tool** | Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface. | -| **Add Attachments** | Embed one or more files into your PDF. | -| **Extract Attachments** | Extract all embedded files from PDF(s) as a ZIP. | -| **Edit Attachments** | View or remove attachments in your PDF. | -| **Divide Pages** | Divide pages horizontally or vertically. | -| **Combine to Single Page**| Stitch all pages into one continuous scroll. | -| **Add Blank Page** | Insert an empty page anywhere in your PDF. | -| **Reverse Pages** | Flip the order of all pages in your document. | -| **View Metadata** | Inspect the hidden properties of your PDF. | -| **PDFs to ZIP** | Package multiple PDF files into a ZIP archive. | -| **Compare PDFs** | Compare two PDFs side by side. | +| Tool Name | Description | +| :------------------------- | :------------------------------------------------------------------------------------------------------ | +| **Merge PDFs** | Combine multiple PDF files into one. | +| **Split PDFs** | Extract specific pages or divide a document into smaller files. | +| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. | +| **Extract Pages** | Save a specific range of pages as a new PDF. | +| **Delete Pages** | Remove unwanted pages from your document. | +| **Rotate PDF** | Rotate individual or all pages in a document. | +| **N-Up PDF** | Combine multiple pages onto a single page. | +| **View PDF** | A powerful, integrated PDF viewer. | +| **Alternate & Mix pages** | Merge pages by alternating pages from each PDF. | +| **Posterize PDF** | Split a PDF into multiple smaller pages for print. | +| **PDF Multi Tool** | Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface. | +| **Add Attachments** | Embed one or more files into your PDF. | +| **Extract Attachments** | Extract all embedded files from PDF(s) as a ZIP. | +| **Edit Attachments** | View or remove attachments in your PDF. | +| **Divide Pages** | Divide pages horizontally or vertically. | +| **Combine to Single Page** | Stitch all pages into one continuous scroll. | +| **Add Blank Page** | Insert an empty page anywhere in your PDF. | +| **Reverse Pages** | Flip the order of all pages in your document. | +| **View Metadata** | Inspect the hidden properties of your PDF. | +| **PDFs to ZIP** | Package multiple PDF files into a ZIP archive. | +| **Compare PDFs** | Compare two PDFs side by side. | ### Edit & Modify PDFs -| Tool Name | Description | -| :--------------------- | :---------------------------------------------------------- | -| **PDF Editor** | A comprehensive editor to modify your PDFs. | +| Tool Name | Description | +| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **PDF Editor** | A comprehensive editor to modify your PDFs. | | **Create Fillable Forms** | Create professional fillable PDF forms with text fields, checkboxes, dropdowns, radio buttons, signatures, and more. Fully compliant with PDF standards for compatibility with all PDF viewers. | -| **Add Page Numbers** | Easily add page numbers with customizable formatting. | -| **Add Watermark** | Add text or image watermarks to protect your documents. | -| **Header & Footer** | Add customizable headers and footers. | -| **Crop PDF** | Crop specific pages or the entire document. | -| **Invert Colors** | Invert the colors of your PDF pages for better readability. | -| **Change Background** | Modify the background color of your PDF. | -| **Change Text Color** | Change the color of text content within the PDF. | -| **Fill Forms** | Fill out PDF forms directly in your browser. | -| **Flatten PDF** | Flatten form fields and annotations into static content. | -| **Remove Annotations** | Remove comments, highlights, and other annotations. | -| **Remove Blank Pages** | Auto detect and remove blank pages in a PDF. | -| **Edit Bookmarks** | Add, Edit, Create, Import and Export PDF Bookmarks. | -| **Add Stamps** | Add image stamps to your PDF using the annotation toolbar. | -| **Table of Contents** | Generate a table of contents page from PDF bookmarks. | -| **Redact Content** | Permanently remove sensitive content from your PDFs. | +| **Add Page Numbers** | Easily add page numbers with customizable formatting. | +| **Add Watermark** | Add text or image watermarks to protect your documents. | +| **Header & Footer** | Add customizable headers and footers. | +| **Crop PDF** | Crop specific pages or the entire document. | +| **Invert Colors** | Invert the colors of your PDF pages for better readability. | +| **Change Background** | Modify the background color of your PDF. | +| **Change Text Color** | Change the color of text content within the PDF. | +| **Fill Forms** | Fill out PDF forms directly in your browser. | +| **Flatten PDF** | Flatten form fields and annotations into static content. | +| **Remove Annotations** | Remove comments, highlights, and other annotations. | +| **Remove Blank Pages** | Auto detect and remove blank pages in a PDF. | +| **Edit Bookmarks** | Add, Edit, Create, Import and Export PDF Bookmarks. | +| **Add Stamps** | Add image stamps to your PDF using the annotation toolbar. | +| **Table of Contents** | Generate a table of contents page from PDF bookmarks. | +| **Redact Content** | Permanently remove sensitive content from your PDFs. | ### Convert to PDF | Tool Name | Description | | :------------------ | :-------------------------------------------------------------- | | **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. | -| **JPG to PDF** | Convert JPG images to PDF. | -| **PNG to PDF** | Convert PNG images to PDF. | -| **WebP to PDF** | Convert WebP images to PDF. | -| **SVG to PDF** | Convert SVG images to PDF. | -| **BMP to PDF** | Convert BMP images to PDF. | -| **HEIC to PDF** | Convert HEIC images to PDF. | -| **TIFF to PDF** | Convert TIFF images to PDF. | +| **JPG to PDF** | Convert JPG images to PDF. | +| **PNG to PDF** | Convert PNG images to PDF. | +| **WebP to PDF** | Convert WebP images to PDF. | +| **SVG to PDF** | Convert SVG images to PDF. | +| **BMP to PDF** | Convert BMP images to PDF. | +| **HEIC to PDF** | Convert HEIC images to PDF. | +| **TIFF to PDF** | Convert TIFF images to PDF. | | **Markdown to PDF** | Convert `.md` files into professional PDF documents. | | **Text to PDF** | Convert plain text files into a PDF. | | **EPUB to PDF** | Convert EPUB e-books to PDF format. | -| **JSON to PDF** | Convert JSON to PDF. | +| **JSON to PDF** | Convert JSON to PDF. | ### Convert from PDF @@ -154,24 +155,24 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs. ### Secure & Optimize PDFs -| Tool Name | Description | -| :--------------------- | :----------------------------------------------------------------- | -| **Compress PDF** | Reduce file size while maintaining quality. | -| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. | -| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. | -| **Decrypt PDF** | Remove password protection from a PDF (password required). | -| **Change Permissions** | Set or modify user permissions for printing, copying, and editing. | -| **Sign PDF** | Add your digital signature to a document. | -| **Digital Signature** | Add cryptographic digital signatures using X.509 certificates (PFX/PEM). | -| **Validate Signature** | Verify digital signatures and view certificate details. | -| **Redact Content** | Permanently remove sensitive content from your PDFs. | -| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). | -| **Remove Metadata** | Strip all metadata from your PDF for privacy. | -| **Linearize PDF** | Optimize PDF for fast web view. | -| **Sanitize PDF** | Remove potentially unwanted or malicous files from PDF. | -| **Fix Page Size** | Standardize all pages to a uniform size. | -| **Page Dimensions** | Analyze page size, orientation, and units. | -| **Remove Restrictions**| Remove password protection and security restrictions associated with digitally signed PDF files. | +| Tool Name | Description | +| :---------------------- | :----------------------------------------------------------------------------------------------- | +| **Compress PDF** | Reduce file size while maintaining quality. | +| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. | +| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. | +| **Decrypt PDF** | Remove password protection from a PDF (password required). | +| **Change Permissions** | Set or modify user permissions for printing, copying, and editing. | +| **Sign PDF** | Add your digital signature to a document. | +| **Digital Signature** | Add cryptographic digital signatures using X.509 certificates (PFX/PEM). | +| **Validate Signature** | Verify digital signatures and view certificate details. | +| **Redact Content** | Permanently remove sensitive content from your PDFs. | +| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). | +| **Remove Metadata** | Strip all metadata from your PDF for privacy. | +| **Linearize PDF** | Optimize PDF for fast web view. | +| **Sanitize PDF** | Remove potentially unwanted or malicous files from PDF. | +| **Fix Page Size** | Standardize all pages to a uniform size. | +| **Page Dimensions** | Analyze page size, orientation, and units. | +| **Remove Restrictions** | Remove password protection and security restrictions associated with digitally signed PDF files. | --- @@ -179,12 +180,13 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs. BentoPDF is available in multiple languages: -| Language | Status | -|------------|--------| -| English | [![English](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/en/common.json) | -| German | [![German](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/de/common.json) | +| Language | Status | +| ---------- | ----------------------------------------------------------------------------------------------------------------- | +| English | [![English](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/en/common.json) | +| German | [![German](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/de/common.json) | | Vietnamese | [![Vietnamese](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/vi/common.json) | -| Chinese | [![Chinese](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/zh/common.json) | +| Chinese | [![Chinese](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/zh/common.json) | +| French | [![French](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/fr/common.json) | Want to help translate BentoPDF into your language? Check out our [Translation Guide](TRANSLATION.md)! @@ -227,7 +229,7 @@ This is the fastest way to try BentoPDF without setting up a development environ ### Static Hosting using Netlify, Vercel, and GitHub Pages -It is very straightforward to host your own instance of BentoPDF using a static web page hosting service. Plus, services such as Netlify, Vercel, and GitHub Pages all offer a free tier for getting started. See [Static Hosting](https://github.com/alam00000/bentopdf/blob/main/STATIC-HOSTING.md)) for details. +It is very straightforward to host your own instance of BentoPDF using a static web page hosting service. Plus, services such as Netlify, Vercel, and GitHub Pages all offer a free tier for getting started. See [Static Hosting](https://github.com/alam00000/bentopdf/blob/main/STATIC-HOSTING.md)) for details. ### 🏠 Self-Hosting Locally @@ -241,7 +243,7 @@ The easiest way to self-host is to download the pre-built distribution file from 2. Download the latest `dist-{version}.zip` file 3. Extract the zip file 4. Serve the extracted folder with your preferred web server - + **Serve the extracted folder (requires Node.js):** ```bash @@ -303,12 +305,12 @@ npm run build:all docker build --build-arg COMPRESSION_MODE=all -t bentopdf:all . ``` -| Mode | Files Kept | Use Case | -|------|------------|----------| -| `g` | `.gz` only | Standard nginx or minimal size | -| `b` | `.br` only | Modern CDN with Brotli support | -| `o` | originals | Development or custom compression | -| `all` | all formats | Maximum compatibility (default) | +| Mode | Files Kept | Use Case | +| ----- | ----------- | --------------------------------- | +| `g` | `.gz` only | Standard nginx or minimal size | +| `b` | `.br` only | Modern CDN with Brotli support | +| `o` | originals | Development or custom compression | +| `all` | all formats | Maximum compatibility (default) | **CDN Optimization:** @@ -323,6 +325,7 @@ npm run build ``` **How it works:** + - When `VITE_USE_CDN=true`: Browser loads WASM files from jsDelivr CDN (fast, global delivery) - Local files are **always included** as automatic fallback - If CDN fails then it falls back to local files @@ -348,7 +351,7 @@ cp -r dist/* serve-test/tools/bentopdf/ npx serve serve-test ``` -The website can be accessible at: ```http://localhost:3000/tools/bentopdf/``` +The website can be accessible at: `http://localhost:3000/tools/bentopdf/` The `npm run package` command creates a `dist-{version}.zip` file that you can use for self-hosting. @@ -378,7 +381,8 @@ docker build \ docker run -p 3000:8080 bentopdf-simple ``` -> **Important**: +> **Important**: +> > - Always include trailing slashes in `BASE_URL` (e.g., `/bentopdf/` not `/bentopdf`) > - The default value is `/` for root deployment @@ -441,6 +445,7 @@ For detailed security configuration, see [SECURITY.md](SECURITY.md). The **Digital Signature** tool uses a signing library that may need to fetch certificate chain data from certificate authority provider. Since many certificate servers don't include CORS headers, a proxy is required for this feature to work in the browser. **When is the proxy needed?** + - Only when using the Digital Signature tool - Only if your certificate requires fetching issuer certificates from external URLs - Self-signed certificates typically don't need this @@ -448,16 +453,19 @@ The **Digital Signature** tool uses a signing library that may need to fetch cer **Deploying the CORS Proxy (Cloudflare Workers):** 1. **Navigate to the cloudflare directory:** + ```bash cd cloudflare ``` 2. **Login to Cloudflare (if not already):** + ```bash npx wrangler login ``` 3. **Deploy the worker:** + ```bash npx wrangler deploy ``` @@ -473,13 +481,13 @@ The **Digital Signature** tool uses a signing library that may need to fetch cer The CORS proxy includes several security measures: -| Feature | Description | -|---------|-------------| -| **URL Restrictions** | Only allows certificate URLs (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`) | -| **Private IP Blocking** | Blocks requests to localhost, 10.x, 192.168.x, 172.16-31.x | -| **File Size Limit** | Rejects files larger than 10MB | -| **Rate Limiting** | 60 requests per IP per minute (requires KV) | -| **HMAC Signatures** | Optional client-side signing (limited protection) | +| Feature | Description | +| ----------------------- | ------------------------------------------------------------------------- | +| **URL Restrictions** | Only allows certificate URLs (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`) | +| **Private IP Blocking** | Blocks requests to localhost, 10.x, 192.168.x, 172.16-31.x | +| **File Size Limit** | Rejects files larger than 10MB | +| **Rate Limiting** | 60 requests per IP per minute (requires KV) | +| **HMAC Signatures** | Optional client-side signing (limited protection) | #### Enabling Rate Limiting (Recommended) @@ -657,6 +665,7 @@ npm run docs:preview ``` Documentation files are in the `docs/` folder: + - `docs/index.md` - Home page - `docs/getting-started.md` - Getting started guide - `docs/tools/` - Tools reference diff --git a/TRANSLATION.md b/TRANSLATION.md index 57dc499bf..e6c3a860c 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -23,6 +23,7 @@ BentoPDF uses **i18next** for internationalization (i18n). Currently supported l - **German** (`de`) - **Vietnamese** (`vi`) - **Indonesian** (`id`) +- **French** (`fr`) The app automatically detects the language from the URL path: @@ -30,6 +31,7 @@ The app automatically detects the language from the URL path: - `/de/` → German - `/vi/` → Vietnamese - `/id/` → Indonesian +- `/fr/` → French --- @@ -54,33 +56,33 @@ The app automatically detects the language from the URL path: ## Adding a New Language -Let's add **French** as an example: +Let's add **Spanish** as an example: ### Step 1: Create Translation File ```bash # Create the directory -mkdir -p public/locales/fr +mkdir -p public/locales/es # Copy the English template -cp public/locales/en/common.json public/locales/fr/common.json +cp public/locales/en/common.json public/locales/es/common.json ``` ### Step 2: Translate the JSON File -Open `public/locales/fr/common.json` and translate all the values: +Open `public/locales/es/common.json` and translate all the values: ```json { "nav": { - "home": "Accueil", - "about": "À propos", - "contact": "Contact", - "allTools": "Tous les outils" + "home": "Inicio", + "about": "Acerca de", + "contact": "Contacto", + "allTools": "Todas las herramientas" }, "hero": { - "title": "Votre boîte à outils PDF gratuite et sécurisée", - "subtitle": "Fusionnez, divisez, compressez et modifiez des PDF directement dans votre navigateur." + "title": "Tu conjunto de herramientas PDF gratuito y seguro", + "subtitle": "Combina, divide, comprime y edita archivos PDF directamente en tu navegador." } // ... continue translating all keys } @@ -91,13 +93,13 @@ Open `public/locales/fr/common.json` and translate all the values: ✅ **Correct:** ```json -"home": "Accueil" +"home": "Inicio" ``` ❌ **Wrong:** ```json -"accueil": "Accueil" +"inicio": "Inicio" ``` ### Step 3: Register the Language @@ -105,8 +107,8 @@ Open `public/locales/fr/common.json` and translate all the values: Edit `src/js/i18n/i18n.ts`: ```typescript -// Add 'fr' to supported languages -export const supportedLanguages = ['en', 'de', 'fr'] as const; +// Add 'es' to supported languages +export const supportedLanguages = ['en', 'de', 'fr', 'es'] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; // Add French display name @@ -132,8 +134,8 @@ const langMatch = url.match(/^\/(en|de|zh|vi|it|fr)(\/.*)?$/); # Start the dev server npm run dev -# Visit the French version -# http://localhost:5173/fr/ +# Visit the Spanish version +# http://localhost:5173/es/ ``` --- @@ -283,7 +285,8 @@ In `common.json`: - German: `http://localhost:5173/de/` - Vietnamese: `http://localhost:5173/vi/` - Indonesian: `http://localhost:5173/id/` - - Your new language: `http://localhost:5173/fr/` + - French: `http://localhost:5173/fr/` + - Your new language: `http://localhost:5173/es/` 3. **Check these pages:** - Homepage (`/`) @@ -506,7 +509,6 @@ Current translation coverage: | English | `en` | ✅ Complete | Core team | | German | `de` | 🚧 In Progress | Core team | | Vietnamese | `vi` | ✅ Complete | Community | -| Indonesian | `id` | ✅ Complete | Community | | Your Language | `??` | 🚧 In Progress | You? | --- diff --git a/nginx.conf b/nginx.conf index a4904f55b..a4396c2fd 100644 --- a/nginx.conf +++ b/nginx.conf @@ -26,7 +26,7 @@ http { root /usr/share/nginx/html; index index.html; - rewrite ^/(en|de|zh|vi|it|tr|id)/(.*)$ /$2 last; + rewrite ^/(en|de|zh|vi|it|tr|id|fr)/(.*)$ /$2 last; location ~* \.html$ { expires 1h; diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json new file mode 100644 index 000000000..c48b51326 --- /dev/null +++ b/public/locales/fr/common.json @@ -0,0 +1,323 @@ +{ + "nav": { + "home": "Accueil", + "about": "À propos", + "contact": "Contact", + "licensing": "Licence", + "allTools": "Tous les outils", + "openMainMenu": "Ouvrir le menu principal", + "language": "Langue" + }, + "donation": { + "message": "Vous aimez BentoPDF ? Aidez-nous à le garder open source !", + "button": "Soutenir" + }, + "hero": { + "title": "La", + "pdfToolkit": "palette d’outils PDF", + "builtForPrivacy": "conçue pour la confidentialité", + "noSignups": "Sans inscription", + "unlimitedUse": "Utilisation illimitée", + "worksOffline": "Fonctionne hors ligne", + "startUsing": "Commencer maintenant" + }, + "usedBy": { + "title": "Utilisé par des entreprises et des professionnels de" + }, + "features": { + "title": "Pourquoi choisir", + "bentoPdf": "BentoPDF ?", + "noSignup": { + "title": "Sans inscription", + "description": "Utilisation immédiate, sans compte ni email." + }, + "noUploads": { + "title": "Aucun envoi de fichiers", + "description": "100 % côté navigateur, vos fichiers ne quittent jamais votre appareil." + }, + "foreverFree": { + "title": "Gratuit pour toujours", + "description": "Tous les outils, sans essai, sans paiement, sans restrictions." + }, + "noLimits": { + "title": "Sans limites", + "description": "Utilisez autant que vous voulez, sans plafonds cachés." + }, + "batchProcessing": { + "title": "Traitement par lots", + "description": "Gérez un nombre illimité de PDF en une seule fois." + }, + "lightningFast": { + "title": "Ultra rapide", + "description": "Traitez vos PDF instantanément, sans attente." + } + }, + "tools": { + "title": "Commencer avec", + "toolsLabel": "Les outils", + "subtitle": "Cliquez sur un outil pour importer vos fichiers", + "searchPlaceholder": "Rechercher un outil (ex. « scinder », « organiser »...)", + "backToTools": "Retour aux outils", + "firstLoadNotice": "Le premier chargement peut prendre quelques instants, le temps de charger notre moteur de conversion. Les prochaines fois, tout se chargera instantanément." + }, + "upload": { + "clickToSelect": "Cliquez pour sélectionner un fichier", + "orDragAndDrop": "ou glissez-déposez", + "pdfOrImages": "PDF ou images", + "filesNeverLeave": "Vos fichiers restent sur votre appareil.", + "addMore": "Ajouter d’autres fichiers", + "clearAll": "Tout effacer" + }, + "loader": { + "processing": "Traitement en cours..." + }, + "alert": { + "title": "Alerte", + "ok": "OK" + }, + "preview": { + "title": "Aperçu du document", + "downloadAsPdf": "Télécharger en PDF", + "close": "Fermer" + }, + "settings": { + "title": "Paramètres", + "shortcuts": "Raccourcis", + "preferences": "Préférences", + "displayPreferences": "Préférences d’affichage", + "searchShortcuts": "Rechercher un raccourci...", + "shortcutsInfo": "Maintenez les touches pour définir un raccourci. Les changements sont enregistrés automatiquement.", + "shortcutsWarning": "⚠️ Évitez les raccourcis courants du navigateur (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N, etc.), ils peuvent ne pas fonctionner correctement.", + "import": "Importer", + "export": "Exporter", + "resetToDefaults": "Rétablir les paramètres par défaut", + "fullWidthMode": "Mode pleine largeur", + "fullWidthDescription": "Utiliser toute la largeur de l’écran au lieu d’un affichage centré", + "settingsAutoSaved": "Les paramètres sont enregistrés automatiquement", + "clickToSet": "Cliquez pour définir", + "pressKeys": "Appuyez sur les touches...", + "warnings": { + "alreadyInUse": "Raccourci déjà utilisé", + "assignedTo": "est déjà attribué à :", + "chooseDifferent": "Veuillez choisir un autre raccourci.", + "reserved": "Avertissement de raccourci réservé", + "commonlyUsed": "est couramment utilisé pour :", + "unreliable": "Ce raccourci peut ne pas fonctionner correctement ou entrer en conflit avec le navigateur ou le système.", + "useAnyway": "Souhaitez-vous l’utiliser quand même ?", + "resetTitle": "Réinitialiser les raccourcis", + "resetMessage": "Êtes-vous sûr de vouloir réinitialiser tous les raccourcis par défaut ?

Cette action est irréversible.", + "importSuccessTitle": "Importation réussie", + "importSuccessMessage": "Les raccourcis ont été importés avec succès !", + "importFailTitle": "Échec de l’importation", + "importFailMessage": "Impossible d’importer les raccourcis. Format de fichier invalide." + } + }, + "warning": { + "title": "Attention", + "cancel": "Annuler", + "proceed": "Continuer" + }, + "compliance": { + "title": "Vos données ne quittent jamais votre appareil", + "weKeep": "Nous protégeons", + "yourInfoSafe": "vos informations", + "byFollowingStandards": "en respectant les normes de sécurité internationales.", + "processingLocal": "Tous les traitements sont effectués localement sur votre appareil.", + "gdpr": { + "title": "Conformité RGPD", + "description": "Protège les données personnelles et la vie privée des citoyens de l’Union européenne." + }, + "ccpa": { + "title": "Conformité CCPA", + "description": "Accorde aux résidents de Californie des droits sur l’utilisation de leurs données personnelles." + }, + "hipaa": { + "title": "Conformité HIPAA", + "description": "Définit des règles strictes pour la gestion des données de santé aux États-Unis." + } + }, + "faq": { + "title": "Questions", + "questions": "fréquentes", + "isFree": { + "question": "BentoPDF est-il vraiment gratuit ?", + "answer": "Oui, totalement. Tous les outils BentoPDF sont 100 % gratuits, sans limite de fichiers, sans inscription et sans filigrane. Nous pensons que chacun doit avoir accès à des outils PDF simples et puissants, sans barrière payante." + }, + "areFilesSecure": { + "question": "Mes fichiers sont-ils en sécurité ? Où sont-ils traités ?", + "answer": "Vos fichiers sont parfaitement sécurisés car ils ne quittent jamais votre ordinateur. Tous les traitements se font directement dans votre navigateur. Aucun fichier n’est envoyé sur un serveur." + }, + "platforms": { + "question": "Est-ce compatible avec Mac, Windows et mobile ?", + "answer": "Oui ! BentoPDF fonctionne entièrement dans le navigateur et est compatible avec Windows, macOS, Linux, iOS et Android." + }, + "gdprCompliant": { + "question": "BentoPDF est-il conforme au RGPD ?", + "answer": "Oui. Comme tous les traitements sont locaux et qu’aucune donnée n’est collectée ou transmise, vous restez entièrement maître de vos documents." + }, + "dataStorage": { + "question": "Stockez-vous ou suivez-vous mes fichiers ?", + "answer": "Non. Aucun stockage, aucun suivi, aucun historique. Tout disparaît dès que vous fermez la page." + }, + "different": { + "question": "Qu’est-ce qui différencie BentoPDF des autres outils PDF ?", + "answer": "La plupart des outils envoient vos fichiers sur un serveur. BentoPDF traite tout localement dans votre navigateur, pour plus de rapidité, de confidentialité et de tranquillité d’esprit." + }, + "browserBased": { + "question": "Pourquoi le traitement dans le navigateur est-il plus sûr ?", + "answer": "Parce que vos fichiers restent sur votre appareil. Aucun risque de fuite, de piratage ou d’accès non autorisé." + }, + "analytics": { + "question": "Utilisez-vous des cookies ou des outils de suivi ?", + "answer": "Nous respectons votre vie privée. BentoPDF utilise uniquement des statistiques anonymes pour connaître le nombre de visites, sans jamais identifier les utilisateurs." + } + }, + "testimonials": { + "title": "Ce que disent", + "users": "nos utilisateurs", + "say": "" + }, + "support": { + "title": "Vous aimez ce projet ?", + "description": "BentoPDF est un projet passion, créé pour offrir une palette d’outils PDF gratuite, privée et puissante. Si cela vous aide, vous pouvez soutenir son développement. Chaque café compte !", + "buyMeCoffee": "M’offrir un café" + }, + "footer": { + "copyright": "© 2025 BentoPDF. Tous droits réservés.", + "version": "Version", + "company": "Entreprise", + "aboutUs": "À propos", + "faqLink": "FAQ", + "contactUs": "Nous contacter", + "legal": "Mentions légales", + "termsAndConditions": "Conditions générales", + "privacyPolicy": "Politique de confidentialité", + "followUs": "Nous suivre" + }, + "merge": { + "title": "Fusionner des PDF", + "description": "Combinez des fichiers entiers ou sélectionnez des pages spécifiques pour créer un nouveau document.", + "fileMode": "Mode fichiers", + "pageMode": "Mode pages", + "howItWorks": "Fonctionnement :", + "fileModeInstructions": [ + "Cliquez-glissez l’icône pour modifier l’ordre des fichiers.", + "Dans le champ « Pages » de chaque fichier, vous pouvez définir des plages (ex. « 1-3, 5 ») pour ne fusionner que certaines pages.", + "Laissez le champ « Pages » vide pour inclure toutes les pages du fichier." + ], + "pageModeInstructions": [ + "Toutes les pages de vos PDF importés s’affichent ci-dessous.", + "Glissez-déposez simplement les miniatures pour définir l’ordre exact de votre nouveau document." + ], + "mergePdfs": "Fusionner les PDF" + }, + "common": { + "page": "Page", + "pages": "Pages", + "of": "sur", + "download": "Télécharger", + "cancel": "Annuler", + "save": "Enregistrer", + "delete": "Supprimer", + "edit": "Modifier", + "add": "Ajouter", + "remove": "Retirer", + "loading": "Chargement...", + "error": "Erreur", + "success": "Succès", + "file": "Fichier", + "files": "Fichiers" + }, + "about": { + "hero": { + "title": "Nous pensons que les outils PDF doivent être", + "subtitle": "rapides, privés et gratuits.", + "noCompromises": "Sans compromis." + }, + "mission": { + "title": "Notre mission", + "description": "Proposer la palette d’outils PDF la plus complète, tout en respectant votre vie privée et sans jamais demander de paiement. Les outils essentiels doivent être accessibles à tous, partout, sans barrières." + }, + "philosophy": { + "label": "Notre philosophie", + "title": "La confidentialité avant tout. Toujours.", + "description": "À une époque où les données sont devenues une monnaie, nous faisons un choix différent. Tous les traitements des outils BentoPDF sont effectués localement dans votre navigateur. Vos fichiers ne passent jamais par nos serveurs, nous ne voyons jamais vos documents et nous ne suivons pas votre activité. Ce n’est pas une option, c’est notre fondation." + }, + "whyBentopdf": { + "title": "Pourquoi", + "speed": { + "title": "Pensé pour la vitesse", + "description": "Aucune attente liée aux envois ou téléchargements serveur. Grâce au traitement local et aux technologies web modernes comme WebAssembly, nos outils sont extrêmement rapides." + }, + "free": { + "title": "Entièrement gratuit", + "description": "Aucun essai, aucun abonnement, aucun coût caché, aucune fonctionnalité « premium » bloquée. Les outils PDF doivent être un service public, pas un produit de luxe." + }, + "noAccount": { + "title": "Aucun compte requis", + "description": "Utilisez n’importe quel outil immédiatement. Pas d’email, pas de mot de passe, aucune donnée personnelle. Votre flux de travail reste fluide et anonyme." + }, + "openSource": { + "title": "Esprit open source", + "description": "Conçu dans un esprit de transparence. Nous utilisons des bibliothèques open source reconnues comme PDF-lib et PDF.js, et croyons en la force de la communauté." + } + }, + "cta": { + "title": "Prêt à commencer ?", + "description": "Rejoignez des milliers d’utilisateurs qui font confiance à BentoPDF au quotidien. Découvrez la différence qu’apportent la confidentialité et la performance.", + "button": "Explorer tous les outils" + } + }, + "contact": { + "title": "Nous contacter", + "subtitle": "Nous serions ravis d’échanger avec vous. Question, retour ou suggestion de fonctionnalité, n’hésitez pas à nous écrire.", + "email": "Vous pouvez nous contacter directement par email à :" + }, + "licensing": { + "title": "Licences pour", + "subtitle": "Choisissez la licence adaptée à vos besoins." + }, + "multiTool": { + "uploadPdfs": "Importer des PDF", + "upload": "Importer", + "addBlankPage": "Ajouter une page vierge", + "edit": "Modifier :", + "undo": "Annuler", + "redo": "Rétablir", + "reset": "Réinitialiser", + "selection": "Sélection :", + "selectAll": "Tout sélectionner", + "deselectAll": "Tout désélectionner", + "rotate": "Rotation :", + "rotateLeft": "Gauche", + "rotateRight": "Droite", + "transform": "Transformer :", + "duplicate": "Dupliquer", + "split": "Scinder", + "clear": "Effacer :", + "delete": "Supprimer", + "download": "Téléchargement :", + "downloadSelected": "Télécharger la sélection", + "exportPdf": "Exporter en PDF", + "uploadPdfFiles": "Sélectionner des fichiers PDF", + "dragAndDrop": "Glissez-déposez vos fichiers PDF ici ou cliquez pour sélectionner", + "selectFiles": "Sélectionner des fichiers", + "renderingPages": "Rendu des pages...", + "actions": { + "duplicatePage": "Dupliquer cette page", + "deletePage": "Supprimer cette page", + "insertPdf": "Insérer un PDF après cette page", + "toggleSplit": "Activer/désactiver la séparation après cette page" + }, + "pleaseWait": "Veuillez patienter", + "pagesRendering": "Les pages sont en cours de rendu. Veuillez patienter...", + "noPagesSelected": "Aucune page sélectionnée", + "selectOnePage": "Veuillez sélectionner au moins une page à télécharger.", + "noPages": "Aucune page", + "noPagesToExport": "Aucune page à exporter.", + "renderingTitle": "Génération des aperçus", + "errorRendering": "Échec du rendu des miniatures", + "error": "Erreur", + "failedToLoad": "Échec du chargement" + } +} \ No newline at end of file diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json new file mode 100644 index 000000000..15a18836a --- /dev/null +++ b/public/locales/fr/tools.json @@ -0,0 +1,282 @@ +{ + "categories": { + "popularTools": "Outils populaires", + "editAnnotate": "Éditer et annoter", + "convertToPdf": "Convertir en PDF", + "convertFromPdf": "Convertir depuis le PDF", + "organizeManage": "Organiser et gérer", + "optimizeRepair": "Optimiser et réparer", + "securePdf": "Sécuriser les PDF" + }, + "pdfMultiTool": { + "name": "Outil PDF tout-en-un", + "subtitle": "Fusionner, scinder, organiser, supprimer, faire pivoter, ajouter des pages vierges, extraire et dupliquer dans une interface unifiée." + }, + "mergePdf": { + "name": "Fusionner des PDF", + "subtitle": "Assembler plusieurs PDF en un seul fichier, tout en conservant les signets." + }, + "splitPdf": { + "name": "Scinder un PDF", + "subtitle": "Extraire une plage de pages dans un nouveau PDF." + }, + "compressPdf": { + "name": "Compresser un PDF", + "subtitle": "Réduire la taille du fichier PDF." + }, + "pdfEditor": { + "name": "Éditeur PDF", + "subtitle": "Annoter, surligner, masquer, commenter, ajouter des formes ou images, rechercher et afficher des PDF." + }, + "jpgToPdf": { + "name": "JPG vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images JPG." + }, + "signPdf": { + "name": "Signer un PDF", + "subtitle": "Dessiner, saisir ou importer votre signature." + }, + "cropPdf": { + "name": "Rogner un PDF", + "subtitle": "Ajuster les marges de chaque page du PDF." + }, + "extractPages": { + "name": "Extraire des pages", + "subtitle": "Enregistrer une sélection de pages dans de nouveaux fichiers." + }, + "duplicateOrganize": { + "name": "Dupliquer et organiser", + "subtitle": "Dupliquer, réorganiser et supprimer des pages." + }, + "deletePages": { + "name": "Supprimer des pages", + "subtitle": "Retirer des pages spécifiques du document." + }, + "editBookmarks": { + "name": "Modifier les signets", + "subtitle": "Ajouter, modifier, importer, supprimer et extraire des signets PDF." + }, + "tableOfContents": { + "name": "Table des matières", + "subtitle": "Générer une table des matières à partir des signets du PDF." + }, + "pageNumbers": { + "name": "Numéros de page", + "subtitle": "Insérer une numérotation dans le document." + }, + "addWatermark": { + "name": "Ajouter un filigrane", + "subtitle": "Apposer un texte ou une image sur les pages du PDF." + }, + "headerFooter": { + "name": "En-tête et pied de page", + "subtitle": "Ajouter du texte en haut et en bas des pages." + }, + "invertColors": { + "name": "Inverser les couleurs", + "subtitle": "Créer une version « mode sombre » du PDF." + }, + "backgroundColor": { + "name": "Couleur de fond", + "subtitle": "Modifier la couleur de fond du PDF." + }, + "changeTextColor": { + "name": "Changer la couleur du texte", + "subtitle": "Modifier la couleur du texte dans le PDF." + }, + "addStamps": { + "name": "Ajouter des tampons", + "subtitle": "Ajouter des tampons image via la barre d’annotations.", + "usernameLabel": "Nom du tampon", + "usernamePlaceholder": "Entrez votre nom (pour les tampons)", + "usernameHint": "Ce nom apparaîtra sur les tampons que vous créez." + }, + "removeAnnotations": { + "name": "Supprimer les annotations", + "subtitle": "Retirer les commentaires, surlignages et liens." + }, + "pdfFormFiller": { + "name": "Remplir un formulaire PDF", + "subtitle": "Remplir des formulaires directement dans le navigateur, y compris les formulaires XFA." + }, + "createPdfForm": { + "name": "Créer un formulaire PDF", + "subtitle": "Créer des formulaires PDF interactifs avec des champs glisser-déposer." + }, + "removeBlankPages": { + "name": "Supprimer les pages blanches", + "subtitle": "Détecter et supprimer automatiquement les pages vides." + }, + "imageToPdf": { + "name": "Images vers PDF", + "subtitle": "Convertir JPG, PNG, WebP, BMP, TIFF, SVG et HEIC en PDF." + }, + "pngToPdf": { + "name": "PNG vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images PNG." + }, + "webpToPdf": { + "name": "WebP vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images WebP." + }, + "svgToPdf": { + "name": "SVG vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images SVG." + }, + "bmpToPdf": { + "name": "BMP vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images BMP." + }, + "heicToPdf": { + "name": "HEIC vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images HEIC." + }, + "tiffToPdf": { + "name": "TIFF vers PDF", + "subtitle": "Créer un PDF à partir d’une ou plusieurs images TIFF." + }, + "textToPdf": { + "name": "Texte vers PDF", + "subtitle": "Convertir un fichier texte en PDF." + }, + "jsonToPdf": { + "name": "JSON vers PDF", + "subtitle": "Convertir des fichiers JSON en PDF." + }, + "pdfToJpg": { + "name": "PDF vers JPG", + "subtitle": "Convertir chaque page du PDF en image JPG." + }, + "pdfToPng": { + "name": "PDF vers PNG", + "subtitle": "Convertir chaque page du PDF en image PNG." + }, + "pdfToWebp": { + "name": "PDF vers WebP", + "subtitle": "Convertir chaque page du PDF en image WebP." + }, + "pdfToBmp": { + "name": "PDF vers BMP", + "subtitle": "Convertir chaque page du PDF en image BMP." + }, + "pdfToTiff": { + "name": "PDF vers TIFF", + "subtitle": "Convertir chaque page du PDF en image TIFF." + }, + "pdfToGreyscale": { + "name": "PDF en niveaux de gris", + "subtitle": "Convertir toutes les couleurs en noir et blanc." + }, + "pdfToJson": { + "name": "PDF vers JSON", + "subtitle": "Convertir des fichiers PDF en JSON." + }, + "ocrPdf": { + "name": "OCR PDF", + "subtitle": "Rendre un PDF consultable et copiable." + }, + "alternateMix": { + "name": "Alterner et mélanger les pages", + "subtitle": "Fusionner des PDF en alternant les pages de chaque fichier, tout en conservant les signets." + }, + "addAttachments": { + "name": "Ajouter des pièces jointes", + "subtitle": "Intégrer un ou plusieurs fichiers dans le PDF." + }, + "extractAttachments": { + "name": "Extraire les pièces jointes", + "subtitle": "Extraire tous les fichiers intégrés des PDF dans une archive ZIP." + }, + "editAttachments": { + "name": "Gérer les pièces jointes", + "subtitle": "Afficher ou supprimer les pièces jointes du PDF." + }, + "dividePages": { + "name": "Diviser les pages", + "subtitle": "Diviser les pages horizontalement ou verticalement." + }, + "addBlankPage": { + "name": "Ajouter une page vierge", + "subtitle": "Insérer une page vide à n’importe quel endroit du PDF." + }, + "reversePages": { + "name": "Inverser l’ordre des pages", + "subtitle": "Renverser l’ordre de toutes les pages du document." + }, + "rotatePdf": { + "name": "Faire pivoter un PDF", + "subtitle": "Tourner les pages par incréments de 90°." + }, + "nUpPdf": { + "name": "PDF N-up", + "subtitle": "Afficher plusieurs pages sur une seule feuille." + }, + "combineToSinglePage": { + "name": "Combiner en une seule page", + "subtitle": "Assembler toutes les pages en un défilement continu." + }, + "viewMetadata": { + "name": "Afficher les métadonnées", + "subtitle": "Consulter les propriétés internes du PDF." + }, + "editMetadata": { + "name": "Modifier les métadonnées", + "subtitle": "Changer l’auteur, le titre et autres propriétés." + }, + "pdfsToZip": { + "name": "PDF vers ZIP", + "subtitle": "Regrouper plusieurs fichiers PDF dans une archive ZIP." + }, + "comparePdfs": { + "name": "Comparer des PDF", + "subtitle": "Comparer deux PDF côte à côte." + }, + "posterizePdf": { + "name": "Posteriser un PDF", + "subtitle": "Découper une grande page en plusieurs pages plus petites." + }, + "fixPageSize": { + "name": "Uniformiser la taille des pages", + "subtitle": "Standardiser toutes les pages à un format identique." + }, + "linearizePdf": { + "name": "Optimiser pour le web", + "subtitle": "Optimiser le PDF pour un affichage rapide en ligne." + }, + "pageDimensions": { + "name": "Dimensions des pages", + "subtitle": "Analyser la taille, l’orientation et les unités des pages." + }, + "removeRestrictions": { + "name": "Supprimer les restrictions", + "subtitle": "Supprimer les protections par mot de passe et restrictions de sécurité des PDF signés." + }, + "repairPdf": { + "name": "Réparer un PDF", + "subtitle": "Récupérer les données de fichiers PDF corrompus ou endommagés." + }, + "encryptPdf": { + "name": "Chiffrer un PDF", + "subtitle": "Protéger le PDF en ajoutant un mot de passe." + }, + "sanitizePdf": { + "name": "Nettoyer un PDF", + "subtitle": "Supprimer les métadonnées, annotations, scripts et autres éléments sensibles." + }, + "decryptPdf": { + "name": "Déverrouiller un PDF", + "subtitle": "Supprimer la protection par mot de passe." + }, + "flattenPdf": { + "name": "Aplatir le PDF", + "subtitle": "Rendre les champs de formulaire et annotations non modifiables." + }, + "removeMetadata": { + "name": "Supprimer les métadonnées", + "subtitle": "Effacer les données cachées du PDF." + }, + "changePermissions": { + "name": "Modifier les autorisations", + "subtitle": "Définir ou modifier les permissions utilisateur du PDF." + } +} \ No newline at end of file diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index ed104643d..7d965ad39 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -5,6 +5,7 @@ import HttpBackend from 'i18next-http-backend'; // Supported languages export const supportedLanguages = [ 'en', + 'fr', 'de', 'zh', 'vi', @@ -16,6 +17,7 @@ export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { en: 'English', + fr: 'Français', de: 'Deutsch', zh: '中文', vi: 'Tiếng Việt', @@ -26,7 +28,7 @@ export const languageNames: Record = { export const getLanguageFromUrl = (): SupportedLanguage => { const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|zh|vi|tr|id|it)(?:\/|$)/); + const langMatch = path.match(/^\/(en|fr|de|zh|vi|tr|id|it)(?:\/|$)/); if ( langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage) @@ -88,9 +90,9 @@ export const changeLanguage = (lang: SupportedLanguage): void => { const currentLang = getLanguageFromUrl(); let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { - newPath = currentPath.replace(/^\/(en|de|zh|vi|tr|id|it)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|zh|vi|tr|id|it)$/)) { + if (currentPath.match(/^\/(en|fr|de|zh|vi|tr|id|it)\//)) { + newPath = currentPath.replace(/^\/(en|fr|de|zh|vi|tr|id|it)\//, `/${lang}/`); + } else if (currentPath.match(/^\/(en|fr|de|zh|vi|tr|id|it)$/)) { newPath = `/${lang}`; } else { newPath = `/${lang}${currentPath}`; @@ -154,7 +156,7 @@ export const rewriteLinks = (): void => { return; } - if (href.match(/^\/(en|de|zh|vi|tr|id|it)\//)) { + if (href.match(/^\/(en|fr|de|zh|vi|tr|id|it)\//)) { return; } let newHref: string; diff --git a/src/pages/encrypt-pdf.html b/src/pages/encrypt-pdf.html index a1f02293a..01ba8e5b5 100644 --- a/src/pages/encrypt-pdf.html +++ b/src/pages/encrypt-pdf.html @@ -1,4 +1,4 @@ - +s diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index 2463b5b5e..00b0a9442 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -63,7 +63,6 @@ Create PDF Form - BentoPDF - diff --git a/vite.config.ts b/vite.config.ts index c4caa121a..b420f015d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ function pagesRewritePlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url?.split('?')[0] || ''; - const langMatch = url.match(/^\/(en|de|zh|vi|it|id|tr)(\/.*)?$/); + const langMatch = url.match(/^\/(en|de|zh|vi|it|id|tr|fr)(\/.*)?$/); if (langMatch) { const lang = langMatch[1]; const restOfPath = langMatch[2] || '/'; From ec830880e7c35033e2bf82fd6752c5de46382848 Mon Sep 17 00:00:00 2001 From: Stanislas MEZUREUX Date: Mon, 12 Jan 2026 00:02:28 +0100 Subject: [PATCH 21/39] chore(i18n): add new translation keys --- public/locales/fr/tools.json | 257 ++++++++++++++++++++++++++++++++++- 1 file changed, 254 insertions(+), 3 deletions(-) diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index 15a18836a..8f36e7c77 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -22,7 +22,29 @@ }, "compressPdf": { "name": "Compresser un PDF", - "subtitle": "Réduire la taille du fichier PDF." + "subtitle": "Réduire la taille du fichier PDF.", + "algorithmLabel": "Algorithme de compression", + "condense": "Condensé (recommandé)", + "photon": "Photon (pour les PDF riches en photos)", + "condenseInfo": "Condensé utilise une compression avancée : suppression du superflu, optimisation des images, sous-ensemble des polices. Idéal pour la plupart des PDF.", + "photonInfo": "Photon convertit les pages en images. À utiliser pour les PDF contenant beaucoup de photos ou scannés.", + "photonWarning": "Attention : le texte ne sera plus sélectionnable et les liens ne fonctionneront plus.", + "levelLabel": "Niveau de compression", + "light": "Léger (préserver la qualité)", + "balanced": "Équilibré (recommandé)", + "aggressive": "Agressif (fichiers plus petits)", + "extreme": "Extrême (compression maximale)", + "grayscale": "Convertir en niveaux de gris", + "grayscaleHint": "Réduit la taille du fichier en supprimant les informations de couleur", + "customSettings": "Paramètres personnalisés", + "customSettingsHint": "Affiner les paramètres de compression :", + "outputQuality": "Qualité de sortie", + "resizeImagesTo": "Redimensionner les images à", + "onlyProcessAbove": "Traiter uniquement au-dessus de", + "removeMetadata": "Supprimer les métadonnées", + "subsetFonts": "Sous-ensemble des polices (supprimer les glyphes inutilisés)", + "removeThumbnails": "Supprimer les vignettes intégrées", + "compressButton": "Compresser le PDF" }, "pdfEditor": { "name": "Éditeur PDF", @@ -109,7 +131,7 @@ }, "imageToPdf": { "name": "Images vers PDF", - "subtitle": "Convertir JPG, PNG, WebP, BMP, TIFF, SVG et HEIC en PDF." + "subtitle": "Convertir un JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP en PDF." }, "pngToPdf": { "name": "PNG vers PDF", @@ -207,6 +229,10 @@ "name": "Faire pivoter un PDF", "subtitle": "Tourner les pages par incréments de 90°." }, + "rotateCustom": { + "name": "Rotation par angle personnalisé", + "subtitle": "Faire pivoter les pages selon un angle personnalisé." + }, "nUpPdf": { "name": "PDF N-up", "subtitle": "Afficher plusieurs pages sur une seule feuille." @@ -278,5 +304,230 @@ "changePermissions": { "name": "Modifier les autorisations", "subtitle": "Définir ou modifier les permissions utilisateur du PDF." + }, + "odtToPdf": { + "name": "ODT vers PDF", + "subtitle": "Convertir des fichiers OpenDocument Text au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers ODT", + "convertButton": "Convertir en PDF" + }, + "csvToPdf": { + "name": "CSV vers PDF", + "subtitle": "Convertir des fichiers tableur CSV au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers CSV", + "convertButton": "Convertir en PDF" + }, + "rtfToPdf": { + "name": "RTF vers PDF", + "subtitle": "Convertir des documents Rich Text Format en PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers RTF", + "convertButton": "Convertir en PDF" + }, + "wordToPdf": { + "name": "Word vers PDF", + "subtitle": "Convertir des documents Word (DOCX, DOC, ODT, RTF) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers DOCX, DOC, ODT, RTF", + "convertButton": "Convertir en PDF" + }, + "excelToPdf": { + "name": "Excel vers PDF", + "subtitle": "Convertir des feuilles de calcul Excel (XLSX, XLS, ODS, CSV) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers XLSX, XLS, ODS, CSV", + "convertButton": "Convertir en PDF" + }, + "powerpointToPdf": { + "name": "PowerPoint vers PDF", + "subtitle": "Convertir des présentations PowerPoint (PPTX, PPT, ODP) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers PPTX, PPT, ODP", + "convertButton": "Convertir en PDF" + }, + "markdownToPdf": { + "name": "Markdown vers PDF", + "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", + "btnSyncScroll": "Synchroniser le défilement", + "btnSettings": "Paramètres", + "btnExportPdf": "Exporter en PDF", + "settingsTitle": "Paramètres Markdown", + "settingsPreset": "Préréglage", + "presetDefault": "Par défaut (type GFM)", + "presetCommonmark": "CommonMark (strict)", + "presetZero": "Minimal (aucune fonctionnalité)", + "settingsOptions": "Options Markdown", + "optAllowHtml": "Autoriser les balises HTML", + "optBreaks": "Convertir les retours à la ligne en
", + "optLinkify": "Convertir automatiquement les URL en liens", + "optTypographer": "Typographie (guillemets intelligents, etc.)" + }, + "pdfBooklet": { + "name": "Livret PDF", + "subtitle": "Réorganiser les pages pour l’impression recto verso en livret. Pliez et agrafez pour créer un livret.", + "howItWorks": "Fonctionnement :", + "step1": "Téléversez un fichier PDF.", + "step2": "Les pages seront réorganisées dans l’ordre du livret.", + "step3": "Imprimez en recto verso, retournement sur le bord court, pliez et agrafez.", + "paperSize": "Format du papier", + "orientation": "Orientation", + "portrait": "Portrait", + "landscape": "Paysage", + "pagesPerSheet": "Pages par feuille", + "createBooklet": "Créer le livret", + "processing": "Traitement...", + "pageCount": "Le nombre de pages sera complété au multiple de 4 si nécessaire." + }, + "xpsToPdf": { + "name": "XPS vers PDF", + "subtitle": "Convertir des documents XPS/OXPS au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers XPS, OXPS", + "convertButton": "Convertir en PDF" + }, + "mobiToPdf": { + "name": "MOBI vers PDF", + "subtitle": "Convertir des livres numériques MOBI au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers MOBI", + "convertButton": "Convertir en PDF" + }, + "epubToPdf": { + "name": "EPUB vers PDF", + "subtitle": "Convertir des livres numériques EPUB au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers EPUB", + "convertButton": "Convertir en PDF" + }, + "fb2ToPdf": { + "name": "FB2 vers PDF", + "subtitle": "Convertir des livres numériques FictionBook (FB2) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers FB2", + "convertButton": "Convertir en PDF" + }, + "cbzToPdf": { + "name": "CBZ vers PDF", + "subtitle": "Convertir des archives de bandes dessinées (CBZ/CBR) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers CBZ, CBR", + "convertButton": "Convertir en PDF" + }, + "wpdToPdf": { + "name": "WPD vers PDF", + "subtitle": "Convertir des documents WordPerfect (WPD) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers WPD", + "convertButton": "Convertir en PDF" + }, + "wpsToPdf": { + "name": "WPS vers PDF", + "subtitle": "Convertir des documents WPS Office au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers WPS", + "convertButton": "Convertir en PDF" + }, + "xmlToPdf": { + "name": "XML vers PDF", + "subtitle": "Convertir des documents XML au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers XML", + "convertButton": "Convertir en PDF" + }, + "pagesToPdf": { + "name": "Pages vers PDF", + "subtitle": "Convertir des documents Apple Pages au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers Pages", + "convertButton": "Convertir en PDF" + }, + "odgToPdf": { + "name": "ODG vers PDF", + "subtitle": "Convertir des fichiers OpenDocument Graphics (ODG) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers ODG", + "convertButton": "Convertir en PDF" + }, + "odsToPdf": { + "name": "ODS vers PDF", + "subtitle": "Convertir des fichiers OpenDocument Spreadsheet (ODS) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers ODS", + "convertButton": "Convertir en PDF" + }, + "odpToPdf": { + "name": "ODP vers PDF", + "subtitle": "Convertir des fichiers OpenDocument Presentation (ODP) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers ODP", + "convertButton": "Convertir en PDF" + }, + "pubToPdf": { + "name": "PUB vers PDF", + "subtitle": "Convertir des fichiers Microsoft Publisher (PUB) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers PUB", + "convertButton": "Convertir en PDF" + }, + "vsdToPdf": { + "name": "VSD vers PDF", + "subtitle": "Convertir des fichiers Microsoft Visio (VSD, VSDX) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers VSD, VSDX", + "convertButton": "Convertir en PDF" + }, + "psdToPdf": { + "name": "PSD vers PDF", + "subtitle": "Convertir des fichiers Adobe Photoshop (PSD) au format PDF. Prend en charge plusieurs fichiers.", + "acceptedFormats": "Fichiers PSD", + "convertButton": "Convertir en PDF" + }, + "pdfToSvg": { + "name": "PDF vers SVG", + "subtitle": "Convertir chaque page d’un fichier PDF en graphique vectoriel évolutif (SVG) pour une qualité parfaite à toutes les tailles." + }, + "extractTables": { + "name": "Extraire les tableaux PDF", + "subtitle": "Extraire les tableaux des fichiers PDF et les exporter en CSV, JSON ou Markdown." + }, + "pdfToCsv": { + "name": "PDF vers CSV", + "subtitle": "Extraire les tableaux d’un PDF et les convertir au format CSV." + }, + "pdfToExcel": { + "name": "PDF vers Excel", + "subtitle": "Extraire les tableaux d’un PDF et les convertir au format Excel (XLSX)." + }, + "pdfToText": { + "name": "PDF vers texte", + "subtitle": "Extraire le texte des fichiers PDF et l’enregistrer en texte brut (.txt). Prend en charge plusieurs fichiers.", + "note": "Cet outil fonctionne UNIQUEMENT avec des PDF créés numériquement. Pour les documents scannés ou les PDF basés sur des images, utilisez plutôt notre outil OCR PDF.", + "convertButton": "Extraire le texte" + }, + "digitalSignPdf": { + "name": "Signature numérique PDF", + "pageTitle": "Signature numérique PDF - Ajouter une signature cryptographique | BentoPDF", + "subtitle": "Ajouter une signature numérique cryptographique à votre PDF à l’aide de certificats X.509. Prend en charge les formats PKCS#12 (.pfx, .p12) et PEM. Votre clé privée ne quitte jamais votre navigateur.", + "certificateSection": "Certificat", + "uploadCert": "Téléverser un certificat (.pfx, .p12)", + "certPassword": "Mot de passe du certificat", + "certPasswordPlaceholder": "Saisissez le mot de passe du certificat", + "certInfo": "Informations du certificat", + "certSubject": "Sujet", + "certIssuer": "Émetteur", + "certValidity": "Validité", + "signatureDetails": "Détails de la signature (facultatif)", + "reason": "Motif", + "reasonPlaceholder": "ex. : J’approuve ce document", + "location": "Lieu", + "locationPlaceholder": "ex. : Paris, France", + "contactInfo": "Coordonnées", + "contactPlaceholder": "ex. : email@exemple.com", + "applySignature": "Appliquer la signature numérique", + "successMessage": "PDF signé avec succès ! La signature peut être vérifiée dans n’importe quel lecteur PDF." + }, + "validateSignaturePdf": { + "name": "Valider la signature PDF", + "pageTitle": "Valider la signature PDF - Vérifier les signatures numériques | BentoPDF", + "subtitle": "Vérifier les signatures numériques de vos fichiers PDF. Contrôlez la validité du certificat, consultez les informations du signataire et confirmez l’intégrité du document. Tout le traitement s’effectue dans votre navigateur." + }, + "emailToPdf": { + "name": "Email vers PDF", + "subtitle": "Convertir des fichiers email (EML, MSG) au format PDF. Prend en charge les exports Outlook et les formats email standards.", + "acceptedFormats": "Fichiers EML, MSG", + "convertButton": "Convertir en PDF" + }, + "fontToOutline": { + "name": "Polices en contours", + "subtitle": "Convertir toutes les polices en contours vectoriels pour un rendu cohérent sur tous les appareils." + }, + "deskewPdf": { + "name": "Redresser un PDF", + "subtitle": "Redresser automatiquement les pages scannées inclinées à l’aide d’OpenCV." } -} \ No newline at end of file +} From cf61212515680e610f348297798f4be094662469 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Mon, 12 Jan 2026 13:30:54 +0530 Subject: [PATCH 22/39] feat(form-creator): add custom date formats, fix duplicate field bug, improve sticky UX Bug Fixes: - Fix duplicate field appearing when removing all options from dropdown/list and dragging - Fix selected tool button ring being clipped by removing overflow-hidden from Fields toolbar Date Format Enhancements: - Add all 30 Adobe Acrobat-compatible date formats - Add ISO 8601 formats (yyyy-mm-dd, yyyy-mm, yyyy) - Add European dot-separated formats (dd.mm.yyyy, dd.mm.yy) - Add date-time formats with 12h/24h time support - Add Custom format option with input field - Add live date format example preview UI/UX Improvements: - Make Properties sidebar sticky on large screens - Make Page Management toolbar sticky below navbar - Make Fields toolbar sticky below Page Management toolbar - Increase Fields toolbar padding for better spacing - Fix browser note text alignment in date properties --- src/js/logic/form-creator.ts | 4357 +++++++++++++++++++--------------- src/pages/form-creator.html | 12 +- 2 files changed, 2419 insertions(+), 1950 deletions(-) diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index a50a33d6c..98854b7a2 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -1,780 +1,918 @@ -import { PDFDocument, StandardFonts, rgb, TextAlignment, PDFName, PDFString, PageSizes, PDFBool, PDFDict, PDFArray, PDFRadioGroup } from 'pdf-lib' -import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js' -import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js' -import { createIcons, icons } from 'lucide' -import * as pdfjsLib from 'pdfjs-dist' -import 'pdfjs-dist/web/pdf_viewer.css' +import { + PDFDocument, + StandardFonts, + rgb, + TextAlignment, + PDFName, + PDFString, + PageSizes, + PDFBool, + PDFDict, + PDFArray, + PDFRadioGroup, +} from 'pdf-lib'; +import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; +import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; +import { createIcons, icons } from 'lucide'; +import * as pdfjsLib from 'pdfjs-dist'; +import 'pdfjs-dist/web/pdf_viewer.css'; // Initialize PDF.js worker -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString() - -import { FormField, PageData } from '../types/index.js' - - -let fields: FormField[] = [] -let selectedField: FormField | null = null -let fieldCounter = 0 -const existingFieldNames: Set = new Set() -const existingRadioGroups: Set = new Set() -let draggedElement: HTMLElement | null = null -let offsetX = 0 -let offsetY = 0 - -let pages: PageData[] = [] -let currentPageIndex = 0 -let uploadedPdfDoc: PDFDocument | null = null -let uploadedPdfjsDoc: any = null -let pageSize: { width: number; height: number } = { width: 612, height: 792 } -let currentScale = 1.333 -let pdfViewerOffset = { x: 0, y: 0 } -let pdfViewerScale = 1.333 - -let resizing = false -let resizeField: FormField | null = null -let resizePos: string | null = null -let startX = 0 -let startY = 0 -let startWidth = 0 -let startHeight = 0 -let startLeft = 0 -let startTop = 0 - -let selectedToolType: string | null = null - -const canvas = document.getElementById('pdfCanvas') as HTMLDivElement -const propertiesPanel = document.getElementById('propertiesPanel') as HTMLDivElement -const fieldCountDisplay = document.getElementById('fieldCount') as HTMLSpanElement -const uploadArea = document.getElementById('upload-area') as HTMLDivElement -const toolContainer = document.getElementById('tool-container') as HTMLDivElement -const dropZone = document.getElementById('dropZone') as HTMLDivElement -const pdfFileInput = document.getElementById('pdfFileInput') as HTMLInputElement -const blankPdfBtn = document.getElementById('blankPdfBtn') as HTMLButtonElement -const pdfUploadInput = document.getElementById('pdfUploadInput') as HTMLInputElement -const pageSizeSelector = document.getElementById('pageSizeSelector') as HTMLDivElement -const pageSizeSelect = document.getElementById('pageSizeSelect') as HTMLSelectElement -const customDimensionsInput = document.getElementById('customDimensionsInput') as HTMLDivElement -const customWidth = document.getElementById('customWidth') as HTMLInputElement -const customHeight = document.getElementById('customHeight') as HTMLInputElement -const confirmBlankBtn = document.getElementById('confirmBlankBtn') as HTMLButtonElement -const pageIndicator = document.getElementById('pageIndicator') as HTMLSpanElement -const prevPageBtn = document.getElementById('prevPageBtn') as HTMLButtonElement -const nextPageBtn = document.getElementById('nextPageBtn') as HTMLButtonElement -const addPageBtn = document.getElementById('addPageBtn') as HTMLButtonElement -const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement -const downloadBtn = document.getElementById('downloadBtn') as HTMLButtonElement -const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null -const gotoPageInput = document.getElementById('gotoPageInput') as HTMLInputElement -const gotoPageBtn = document.getElementById('gotoPageBtn') as HTMLButtonElement - -const gridVInput = document.getElementById('gridVInput') as HTMLInputElement -const gridHInput = document.getElementById('gridHInput') as HTMLInputElement -const toggleGridBtn = document.getElementById('toggleGridBtn') as HTMLButtonElement -const enableGridCheckbox = document.getElementById('enableGridCheckbox') as HTMLInputElement -let gridV = 2 -let gridH = 2 -let gridAlwaysVisible = false -let gridEnabled = true +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); + +import { FormField, PageData } from '../types/index.js'; + +let fields: FormField[] = []; +let selectedField: FormField | null = null; +let fieldCounter = 0; +const existingFieldNames: Set = new Set(); +const existingRadioGroups: Set = new Set(); +let draggedElement: HTMLElement | null = null; +let offsetX = 0; +let offsetY = 0; + +let pages: PageData[] = []; +let currentPageIndex = 0; +let uploadedPdfDoc: PDFDocument | null = null; +let uploadedPdfjsDoc: any = null; +let pageSize: { width: number; height: number } = { width: 612, height: 792 }; +let currentScale = 1.333; +let pdfViewerOffset = { x: 0, y: 0 }; +let pdfViewerScale = 1.333; + +let resizing = false; +let resizeField: FormField | null = null; +let resizePos: string | null = null; +let startX = 0; +let startY = 0; +let startWidth = 0; +let startHeight = 0; +let startLeft = 0; +let startTop = 0; + +let selectedToolType: string | null = null; + +const canvas = document.getElementById('pdfCanvas') as HTMLDivElement; +const propertiesPanel = document.getElementById( + 'propertiesPanel' +) as HTMLDivElement; +const fieldCountDisplay = document.getElementById( + 'fieldCount' +) as HTMLSpanElement; +const uploadArea = document.getElementById('upload-area') as HTMLDivElement; +const toolContainer = document.getElementById( + 'tool-container' +) as HTMLDivElement; +const dropZone = document.getElementById('dropZone') as HTMLDivElement; +const pdfFileInput = document.getElementById( + 'pdfFileInput' +) as HTMLInputElement; +const blankPdfBtn = document.getElementById('blankPdfBtn') as HTMLButtonElement; +const pdfUploadInput = document.getElementById( + 'pdfUploadInput' +) as HTMLInputElement; +const pageSizeSelector = document.getElementById( + 'pageSizeSelector' +) as HTMLDivElement; +const pageSizeSelect = document.getElementById( + 'pageSizeSelect' +) as HTMLSelectElement; +const customDimensionsInput = document.getElementById( + 'customDimensionsInput' +) as HTMLDivElement; +const customWidth = document.getElementById('customWidth') as HTMLInputElement; +const customHeight = document.getElementById( + 'customHeight' +) as HTMLInputElement; +const confirmBlankBtn = document.getElementById( + 'confirmBlankBtn' +) as HTMLButtonElement; +const pageIndicator = document.getElementById( + 'pageIndicator' +) as HTMLSpanElement; +const prevPageBtn = document.getElementById('prevPageBtn') as HTMLButtonElement; +const nextPageBtn = document.getElementById('nextPageBtn') as HTMLButtonElement; +const addPageBtn = document.getElementById('addPageBtn') as HTMLButtonElement; +const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement; +const downloadBtn = document.getElementById('downloadBtn') as HTMLButtonElement; +const backToToolsBtn = document.getElementById( + 'back-to-tools' +) as HTMLButtonElement | null; +const gotoPageInput = document.getElementById( + 'gotoPageInput' +) as HTMLInputElement; +const gotoPageBtn = document.getElementById('gotoPageBtn') as HTMLButtonElement; + +const gridVInput = document.getElementById('gridVInput') as HTMLInputElement; +const gridHInput = document.getElementById('gridHInput') as HTMLInputElement; +const toggleGridBtn = document.getElementById( + 'toggleGridBtn' +) as HTMLButtonElement; +const enableGridCheckbox = document.getElementById( + 'enableGridCheckbox' +) as HTMLInputElement; +let gridV = 2; +let gridH = 2; +let gridAlwaysVisible = false; +let gridEnabled = true; if (gridVInput && gridHInput) { - gridVInput.value = '2' - gridHInput.value = '2' + gridVInput.value = '2'; + gridHInput.value = '2'; - const updateGrid = () => { - let v = parseInt(gridVInput.value) || 2 - let h = parseInt(gridHInput.value) || 2 + const updateGrid = () => { + let v = parseInt(gridVInput.value) || 2; + let h = parseInt(gridHInput.value) || 2; - if (v < 2) { v = 2; gridVInput.value = '2' } - if (h < 2) { h = 2; gridHInput.value = '2' } - if (v > 14) { v = 14; gridVInput.value = '14' } - if (h > 14) { h = 14; gridHInput.value = '14' } + if (v < 2) { + v = 2; + gridVInput.value = '2'; + } + if (h < 2) { + h = 2; + gridHInput.value = '2'; + } + if (v > 14) { + v = 14; + gridVInput.value = '14'; + } + if (h > 14) { + h = 14; + gridHInput.value = '14'; + } - gridV = v - gridH = h + gridV = v; + gridH = h; - if (gridAlwaysVisible && gridEnabled) { - renderGrid() - } + if (gridAlwaysVisible && gridEnabled) { + renderGrid(); } + }; - gridVInput.addEventListener('input', updateGrid) - gridHInput.addEventListener('input', updateGrid) + gridVInput.addEventListener('input', updateGrid); + gridHInput.addEventListener('input', updateGrid); } if (enableGridCheckbox) { - enableGridCheckbox.addEventListener('change', (e) => { - gridEnabled = (e.target as HTMLInputElement).checked - - if (!gridEnabled) { - removeGrid() - if (gridVInput) gridVInput.disabled = true - if (gridHInput) gridHInput.disabled = true - if (toggleGridBtn) toggleGridBtn.disabled = true - } else { - if (gridVInput) gridVInput.disabled = false - if (gridHInput) gridHInput.disabled = false - if (toggleGridBtn) toggleGridBtn.disabled = false - if (gridAlwaysVisible) renderGrid() - } - }) + enableGridCheckbox.addEventListener('change', (e) => { + gridEnabled = (e.target as HTMLInputElement).checked; + + if (!gridEnabled) { + removeGrid(); + if (gridVInput) gridVInput.disabled = true; + if (gridHInput) gridHInput.disabled = true; + if (toggleGridBtn) toggleGridBtn.disabled = true; + } else { + if (gridVInput) gridVInput.disabled = false; + if (gridHInput) gridHInput.disabled = false; + if (toggleGridBtn) toggleGridBtn.disabled = false; + if (gridAlwaysVisible) renderGrid(); + } + }); } if (toggleGridBtn) { - toggleGridBtn.addEventListener('click', () => { - gridAlwaysVisible = !gridAlwaysVisible + toggleGridBtn.addEventListener('click', () => { + gridAlwaysVisible = !gridAlwaysVisible; - if (gridAlwaysVisible) { - toggleGridBtn.classList.add('bg-indigo-600') - toggleGridBtn.classList.remove('bg-gray-600') - if (gridEnabled) renderGrid() - } else { - toggleGridBtn.classList.remove('bg-indigo-600') - toggleGridBtn.classList.add('bg-gray-600') - removeGrid() - } - }) + if (gridAlwaysVisible) { + toggleGridBtn.classList.add('bg-indigo-600'); + toggleGridBtn.classList.remove('bg-gray-600'); + if (gridEnabled) renderGrid(); + } else { + toggleGridBtn.classList.remove('bg-indigo-600'); + toggleGridBtn.classList.add('bg-gray-600'); + removeGrid(); + } + }); } function renderGrid() { - const existingGrid = document.getElementById('pdfGrid') - if (existingGrid) existingGrid.remove() - - const gridContainer = document.createElement('div') - gridContainer.id = 'pdfGrid' - gridContainer.className = 'absolute inset-0 pointer-events-none' - gridContainer.style.zIndex = '1' - - if (gridV > 0) { - const stepX = canvas.offsetWidth / gridV - for (let i = 0; i <= gridV; i++) { - const line = document.createElement('div') - line.className = 'absolute top-0 bottom-0 border-l-2 border-indigo-500 opacity-60' - line.style.left = (i * stepX) + 'px' - gridContainer.appendChild(line) - } + const existingGrid = document.getElementById('pdfGrid'); + if (existingGrid) existingGrid.remove(); + + const gridContainer = document.createElement('div'); + gridContainer.id = 'pdfGrid'; + gridContainer.className = 'absolute inset-0 pointer-events-none'; + gridContainer.style.zIndex = '1'; + + if (gridV > 0) { + const stepX = canvas.offsetWidth / gridV; + for (let i = 0; i <= gridV; i++) { + const line = document.createElement('div'); + line.className = + 'absolute top-0 bottom-0 border-l-2 border-indigo-500 opacity-60'; + line.style.left = i * stepX + 'px'; + gridContainer.appendChild(line); } - - if (gridH > 0) { - const stepY = canvas.offsetHeight / gridH - for (let i = 0; i <= gridH; i++) { - const line = document.createElement('div') - line.className = 'absolute left-0 right-0 border-t-2 border-indigo-500 opacity-60' - line.style.top = (i * stepY) + 'px' - gridContainer.appendChild(line) - } + } + + if (gridH > 0) { + const stepY = canvas.offsetHeight / gridH; + for (let i = 0; i <= gridH; i++) { + const line = document.createElement('div'); + line.className = + 'absolute left-0 right-0 border-t-2 border-indigo-500 opacity-60'; + line.style.top = i * stepY + 'px'; + gridContainer.appendChild(line); } + } - canvas.insertBefore(gridContainer, canvas.firstChild) + canvas.insertBefore(gridContainer, canvas.firstChild); } function removeGrid() { - const existingGrid = document.getElementById('pdfGrid') - if (existingGrid) existingGrid.remove() + const existingGrid = document.getElementById('pdfGrid'); + if (existingGrid) existingGrid.remove(); } if (gotoPageBtn && gotoPageInput) { - gotoPageBtn.addEventListener('click', () => { - const pageNum = parseInt(gotoPageInput.value) - if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { - currentPageIndex = pageNum - 1 - renderCanvas() - updatePageNavigation() - } else { - alert(`Please enter a valid page number between 1 and ${pages.length}`) - } - }) + gotoPageBtn.addEventListener('click', () => { + const pageNum = parseInt(gotoPageInput.value); + if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { + currentPageIndex = pageNum - 1; + renderCanvas(); + updatePageNavigation(); + } else { + alert(`Please enter a valid page number between 1 and ${pages.length}`); + } + }); - gotoPageInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - gotoPageBtn.click() - } - }) + gotoPageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + gotoPageBtn.click(); + } + }); } // Tool item interactions -const toolItems = document.querySelectorAll('.tool-item') -toolItems.forEach(item => { - // Drag from toolbar - item.addEventListener('dragstart', (e) => { - if (e instanceof DragEvent && e.dataTransfer) { - e.dataTransfer.effectAllowed = 'copy' - const type = (item as HTMLElement).dataset.type || 'text' - e.dataTransfer.setData('text/plain', type) - if (gridEnabled) renderGrid() - } - }) - - item.addEventListener('dragend', () => { - if (!gridAlwaysVisible && gridEnabled) removeGrid() - }) - item.addEventListener('click', () => { - const type = (item as HTMLElement).dataset.type || 'text' - - // Toggle selection - if (selectedToolType === type) { - // Deselect - selectedToolType = null - item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600') - canvas.style.cursor = 'default' - } else { - // Deselect previous tool - if (selectedToolType) { - toolItems.forEach(t => t.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600')) - } - // Select new tool - selectedToolType = type - item.classList.add('ring-2', 'ring-indigo-400', 'bg-indigo-600') - canvas.style.cursor = 'crosshair' - } - }) - - // Touch events for mobile drag - let touchStartX = 0 - let touchStartY = 0 - let isTouchDragging = false - - item.addEventListener('touchstart', (e) => { - const touch = e.touches[0] - touchStartX = touch.clientX - touchStartY = touch.clientY - isTouchDragging = false - }) - - item.addEventListener('touchmove', (e) => { - e.preventDefault() // Prevent scrolling while dragging - const touch = e.touches[0] - const moveX = Math.abs(touch.clientX - touchStartX) - const moveY = Math.abs(touch.clientY - touchStartY) - - // If moved more than 10px, it's a drag not a click - if (moveX > 10 || moveY > 10) { - isTouchDragging = true - } - }) - - item.addEventListener('touchend', (e) => { - e.preventDefault() - if (!isTouchDragging) { - // It was a tap, treat as click - (item as HTMLElement).click() - return - } +const toolItems = document.querySelectorAll('.tool-item'); +toolItems.forEach((item) => { + // Drag from toolbar + item.addEventListener('dragstart', (e) => { + if (e instanceof DragEvent && e.dataTransfer) { + e.dataTransfer.effectAllowed = 'copy'; + const type = (item as HTMLElement).dataset.type || 'text'; + e.dataTransfer.setData('text/plain', type); + if (gridEnabled) renderGrid(); + } + }); + + item.addEventListener('dragend', () => { + if (!gridAlwaysVisible && gridEnabled) removeGrid(); + }); + item.addEventListener('click', () => { + const type = (item as HTMLElement).dataset.type || 'text'; + + // Toggle selection + if (selectedToolType === type) { + // Deselect + selectedToolType = null; + item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600'); + canvas.style.cursor = 'default'; + } else { + // Deselect previous tool + if (selectedToolType) { + toolItems.forEach((t) => + t.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600') + ); + } + // Select new tool + selectedToolType = type; + item.classList.add('ring-2', 'ring-indigo-400', 'bg-indigo-600'); + canvas.style.cursor = 'crosshair'; + } + }); + + // Touch events for mobile drag + let touchStartX = 0; + let touchStartY = 0; + let isTouchDragging = false; + + item.addEventListener('touchstart', (e) => { + const touch = e.touches[0]; + touchStartX = touch.clientX; + touchStartY = touch.clientY; + isTouchDragging = false; + }); + + item.addEventListener('touchmove', (e) => { + e.preventDefault(); // Prevent scrolling while dragging + const touch = e.touches[0]; + const moveX = Math.abs(touch.clientX - touchStartX); + const moveY = Math.abs(touch.clientY - touchStartY); + + // If moved more than 10px, it's a drag not a click + if (moveX > 10 || moveY > 10) { + isTouchDragging = true; + } + }); + + item.addEventListener('touchend', (e) => { + e.preventDefault(); + if (!isTouchDragging) { + // It was a tap, treat as click + (item as HTMLElement).click(); + return; + } - // It was a drag, place field at touch end position - const touch = e.changedTouches[0] - const canvasRect = canvas.getBoundingClientRect() - - // Check if touch ended on canvas - if (touch.clientX >= canvasRect.left && touch.clientX <= canvasRect.right && - touch.clientY >= canvasRect.top && touch.clientY <= canvasRect.bottom) { - const x = touch.clientX - canvasRect.left - 75 - const y = touch.clientY - canvasRect.top - 15 - const type = (item as HTMLElement).dataset.type || 'text' - createField(type as any, x, y) - } - }) -}) + // It was a drag, place field at touch end position + const touch = e.changedTouches[0]; + const canvasRect = canvas.getBoundingClientRect(); + + // Check if touch ended on canvas + if ( + touch.clientX >= canvasRect.left && + touch.clientX <= canvasRect.right && + touch.clientY >= canvasRect.top && + touch.clientY <= canvasRect.bottom + ) { + const x = touch.clientX - canvasRect.left - 75; + const y = touch.clientY - canvasRect.top - 15; + const type = (item as HTMLElement).dataset.type || 'text'; + createField(type as any, x, y); + } + }); +}); // Canvas drop zone canvas.addEventListener('dragover', (e) => { - e.preventDefault() - if (e.dataTransfer) { - e.dataTransfer.dropEffect = 'copy' - } -}) + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } +}); canvas.addEventListener('drop', (e) => { - e.preventDefault() - if (!gridAlwaysVisible) removeGrid() - const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - 75 - const y = e.clientY - rect.top - 15 - const type = e.dataTransfer?.getData('text/plain') || 'text' - createField(type as any, x, y) -}) + e.preventDefault(); + if (!gridAlwaysVisible) removeGrid(); + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left - 75; + const y = e.clientY - rect.top - 15; + const type = e.dataTransfer?.getData('text/plain') || 'text'; + createField(type as any, x, y); +}); canvas.addEventListener('click', (e) => { - if (selectedToolType) { - const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - 75 - const y = e.clientY - rect.top - 15 - createField(selectedToolType as any, x, y) - - toolItems.forEach(item => item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600')) - selectedToolType = null - canvas.style.cursor = 'default' - return - } - - // Existing deselect behavior (only if no tool is selected) - if (e.target === canvas) { - deselectAll() - } -}) + if (selectedToolType) { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left - 75; + const y = e.clientY - rect.top - 15; + createField(selectedToolType as any, x, y); + + toolItems.forEach((item) => + item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600') + ); + selectedToolType = null; + canvas.style.cursor = 'default'; + return; + } + + // Existing deselect behavior (only if no tool is selected) + if (e.target === canvas) { + deselectAll(); + } +}); function createField(type: FormField['type'], x: number, y: number): void { - fieldCounter++ - const field: FormField = { - id: `field_${fieldCounter}`, - type: type, - x: Math.max(0, Math.min(x, 816 - 150)), - y: Math.max(0, Math.min(y, 1056 - 30)), - width: type === 'checkbox' || type === 'radio' ? 30 : 150, - height: type === 'checkbox' || type === 'radio' ? 30 : 30, - name: `${type.charAt(0).toUpperCase() + type.slice(1)}_${fieldCounter}`, - defaultValue: '', - fontSize: 12, - alignment: 'left', - textColor: '#000000', - required: false, - readOnly: false, - tooltip: '', - combCells: 0, - maxLength: 0, - options: type === 'dropdown' || type === 'optionlist' ? ['Option 1', 'Option 2', 'Option 3'] : undefined, - checked: type === 'radio' || type === 'checkbox' ? false : undefined, - exportValue: type === 'radio' || type === 'checkbox' ? 'Yes' : undefined, - groupName: type === 'radio' ? 'RadioGroup1' : undefined, - label: type === 'button' ? 'Button' : (type === 'image' ? 'Click to Upload Image' : undefined), - action: type === 'button' ? 'none' : undefined, - jsScript: type === 'button' ? 'app.alert("Hello World!");' : undefined, - visibilityAction: type === 'button' ? 'toggle' : undefined, - dateFormat: type === 'date' ? 'mm/dd/yyyy' : undefined, - pageIndex: currentPageIndex, - multiline: type === 'text' ? false : undefined, - borderColor: '#000000', - hideBorder: false - } - - fields.push(field) - renderField(field) - updateFieldCount() + fieldCounter++; + const field: FormField = { + id: `field_${fieldCounter}`, + type: type, + x: Math.max(0, Math.min(x, 816 - 150)), + y: Math.max(0, Math.min(y, 1056 - 30)), + width: type === 'checkbox' || type === 'radio' ? 30 : 150, + height: type === 'checkbox' || type === 'radio' ? 30 : 30, + name: `${type.charAt(0).toUpperCase() + type.slice(1)}_${fieldCounter}`, + defaultValue: '', + fontSize: 12, + alignment: 'left', + textColor: '#000000', + required: false, + readOnly: false, + tooltip: '', + combCells: 0, + maxLength: 0, + options: + type === 'dropdown' || type === 'optionlist' + ? ['Option 1', 'Option 2', 'Option 3'] + : undefined, + checked: type === 'radio' || type === 'checkbox' ? false : undefined, + exportValue: type === 'radio' || type === 'checkbox' ? 'Yes' : undefined, + groupName: type === 'radio' ? 'RadioGroup1' : undefined, + label: + type === 'button' + ? 'Button' + : type === 'image' + ? 'Click to Upload Image' + : undefined, + action: type === 'button' ? 'none' : undefined, + jsScript: type === 'button' ? 'app.alert("Hello World!");' : undefined, + visibilityAction: type === 'button' ? 'toggle' : undefined, + dateFormat: type === 'date' ? 'mm/dd/yyyy' : undefined, + pageIndex: currentPageIndex, + multiline: type === 'text' ? false : undefined, + borderColor: '#000000', + hideBorder: false, + }; + + fields.push(field); + renderField(field); + updateFieldCount(); } // Render field on canvas function renderField(field: FormField): void { - const fieldWrapper = document.createElement('div') - fieldWrapper.id = field.id - fieldWrapper.className = 'absolute cursor-move group' // Added group for hover effects - fieldWrapper.style.left = field.x + 'px' - fieldWrapper.style.top = field.y + 'px' - fieldWrapper.style.width = field.width + 'px' - fieldWrapper.style.overflow = 'visible' - fieldWrapper.style.zIndex = '10' // Ensure fields are above grid and PDF - - // Create label - hidden by default, shown on group hover or selection - const label = document.createElement('div') - label.className = 'field-label absolute left-0 w-full text-xs font-semibold pointer-events-none select-none opacity-0 group-hover:opacity-100 transition-opacity' - label.style.bottom = '100%' - label.style.marginBottom = '4px' - label.style.color = '#374151' - label.style.fontSize = '11px' - label.style.lineHeight = '1' - label.style.whiteSpace = 'nowrap' - label.style.overflow = 'hidden' - label.style.textOverflow = 'ellipsis' - label.textContent = field.name - - // Create input container - light border by default, dashed on hover - const fieldContainer = document.createElement('div') - fieldContainer.className = - 'field-container relative border-2 border-indigo-200 group-hover:border-dashed group-hover:border-indigo-300 bg-indigo-50/30 rounded transition-all' - fieldContainer.style.width = '100%' - fieldContainer.style.height = field.height + 'px' - - // Create content based on type - const contentEl = document.createElement('div') - contentEl.className = 'field-content w-full h-full flex items-center justify-center overflow-hidden' - - if (field.type === 'text') { - contentEl.className = 'field-text w-full h-full flex items-center px-2 text-sm overflow-hidden' - contentEl.style.fontSize = field.fontSize + 'px' - contentEl.style.textAlign = field.alignment - contentEl.style.justifyContent = field.alignment === 'left' ? 'flex-start' : field.alignment === 'right' ? 'flex-end' : 'center' - contentEl.style.color = field.textColor - contentEl.style.whiteSpace = field.multiline ? 'pre-wrap' : 'nowrap' - contentEl.style.textOverflow = 'ellipsis' - contentEl.style.alignItems = field.multiline ? 'flex-start' : 'center' - contentEl.textContent = field.defaultValue - - // Apply combing visual if enabled - if (field.combCells > 0) { - contentEl.style.backgroundImage = `repeating-linear-gradient(90deg, transparent, transparent calc((100% / ${field.combCells}) - 1px), #e5e7eb calc((100% / ${field.combCells}) - 1px), #e5e7eb calc(100% / ${field.combCells}))` - contentEl.style.fontFamily = 'monospace' - contentEl.style.letterSpacing = `calc(${field.width / field.combCells}px - 1ch)` - contentEl.style.paddingLeft = `calc((${field.width / field.combCells}px - 1ch) / 2)` - contentEl.style.overflow = 'hidden' - contentEl.style.textAlign = 'left' - contentEl.style.justifyContent = 'flex-start' - } - } else if (field.type === 'checkbox') { - contentEl.innerHTML = field.checked ? '' : '' - } else if (field.type === 'radio') { - fieldContainer.classList.add('rounded-full') // Make container round for radio - contentEl.innerHTML = field.checked ? '
' : '' - } else if (field.type === 'dropdown') { - contentEl.className = 'w-full h-full flex items-center px-2 text-sm text-black' - contentEl.style.backgroundColor = '#e6f0ff' // Light blue background like Firefox - - // Show selected option or first option or placeholder - let displayText = 'Select...' - if (field.defaultValue && field.options && field.options.includes(field.defaultValue)) { - displayText = field.defaultValue - } else if (field.options && field.options.length > 0) { - displayText = field.options[0] - } - contentEl.textContent = displayText - - const arrow = document.createElement('div') - arrow.className = 'absolute right-1 top-1/2 -translate-y-1/2' - arrow.innerHTML = '' - fieldContainer.appendChild(arrow) - - } else if (field.type === 'optionlist') { - contentEl.className = 'w-full h-full flex flex-col text-sm bg-white overflow-hidden border border-gray-300' - // Render options as a list - if (field.options && field.options.length > 0) { - field.options.forEach((opt, index) => { - const optEl = document.createElement('div') - optEl.className = 'px-1 w-full truncate' - optEl.textContent = opt - - // Highlight selected option (defaultValue) or first one if no selection - const isSelected = field.defaultValue ? field.defaultValue === opt : index === 0 - - if (isSelected) { - optEl.className += ' bg-blue-600 text-white' - } else { - optEl.className += ' text-black' - } - contentEl.appendChild(optEl) - }) + const existingField = document.getElementById(field.id); + if (existingField) { + existingField.remove(); + } + + const fieldWrapper = document.createElement('div'); + fieldWrapper.id = field.id; + fieldWrapper.className = 'absolute cursor-move group'; // Added group for hover effects + fieldWrapper.style.left = field.x + 'px'; + fieldWrapper.style.top = field.y + 'px'; + fieldWrapper.style.width = field.width + 'px'; + fieldWrapper.style.overflow = 'visible'; + fieldWrapper.style.zIndex = '10'; // Ensure fields are above grid and PDF + + // Create label - hidden by default, shown on group hover or selection + const label = document.createElement('div'); + label.className = + 'field-label absolute left-0 w-full text-xs font-semibold pointer-events-none select-none opacity-0 group-hover:opacity-100 transition-opacity'; + label.style.bottom = '100%'; + label.style.marginBottom = '4px'; + label.style.color = '#374151'; + label.style.fontSize = '11px'; + label.style.lineHeight = '1'; + label.style.whiteSpace = 'nowrap'; + label.style.overflow = 'hidden'; + label.style.textOverflow = 'ellipsis'; + label.textContent = field.name; + + // Create input container - light border by default, dashed on hover + const fieldContainer = document.createElement('div'); + fieldContainer.className = + 'field-container relative border-2 border-indigo-200 group-hover:border-dashed group-hover:border-indigo-300 bg-indigo-50/30 rounded transition-all'; + fieldContainer.style.width = '100%'; + fieldContainer.style.height = field.height + 'px'; + + // Create content based on type + const contentEl = document.createElement('div'); + contentEl.className = + 'field-content w-full h-full flex items-center justify-center overflow-hidden'; + + if (field.type === 'text') { + contentEl.className = + 'field-text w-full h-full flex items-center px-2 text-sm overflow-hidden'; + contentEl.style.fontSize = field.fontSize + 'px'; + contentEl.style.textAlign = field.alignment; + contentEl.style.justifyContent = + field.alignment === 'left' + ? 'flex-start' + : field.alignment === 'right' + ? 'flex-end' + : 'center'; + contentEl.style.color = field.textColor; + contentEl.style.whiteSpace = field.multiline ? 'pre-wrap' : 'nowrap'; + contentEl.style.textOverflow = 'ellipsis'; + contentEl.style.alignItems = field.multiline ? 'flex-start' : 'center'; + contentEl.textContent = field.defaultValue; + + // Apply combing visual if enabled + if (field.combCells > 0) { + contentEl.style.backgroundImage = `repeating-linear-gradient(90deg, transparent, transparent calc((100% / ${field.combCells}) - 1px), #e5e7eb calc((100% / ${field.combCells}) - 1px), #e5e7eb calc(100% / ${field.combCells}))`; + contentEl.style.fontFamily = 'monospace'; + contentEl.style.letterSpacing = `calc(${field.width / field.combCells}px - 1ch)`; + contentEl.style.paddingLeft = `calc((${field.width / field.combCells}px - 1ch) / 2)`; + contentEl.style.overflow = 'hidden'; + contentEl.style.textAlign = 'left'; + contentEl.style.justifyContent = 'flex-start'; + } + } else if (field.type === 'checkbox') { + contentEl.innerHTML = field.checked + ? '' + : ''; + } else if (field.type === 'radio') { + fieldContainer.classList.add('rounded-full'); // Make container round for radio + contentEl.innerHTML = field.checked + ? '
' + : ''; + } else if (field.type === 'dropdown') { + contentEl.className = + 'w-full h-full flex items-center px-2 text-sm text-black'; + contentEl.style.backgroundColor = '#e6f0ff'; // Light blue background like Firefox + + // Show selected option or first option or placeholder + let displayText = 'Select...'; + if ( + field.defaultValue && + field.options && + field.options.includes(field.defaultValue) + ) { + displayText = field.defaultValue; + } else if (field.options && field.options.length > 0) { + displayText = field.options[0]; + } + contentEl.textContent = displayText; + + const arrow = document.createElement('div'); + arrow.className = 'absolute right-1 top-1/2 -translate-y-1/2'; + arrow.innerHTML = + ''; + fieldContainer.appendChild(arrow); + } else if (field.type === 'optionlist') { + contentEl.className = + 'w-full h-full flex flex-col text-sm bg-white overflow-hidden border border-gray-300'; + // Render options as a list + if (field.options && field.options.length > 0) { + field.options.forEach((opt, index) => { + const optEl = document.createElement('div'); + optEl.className = 'px-1 w-full truncate'; + optEl.textContent = opt; + + // Highlight selected option (defaultValue) or first one if no selection + const isSelected = field.defaultValue + ? field.defaultValue === opt + : index === 0; + + if (isSelected) { + optEl.className += ' bg-blue-600 text-white'; } else { - // Empty state - const optEl = document.createElement('div') - optEl.className = 'px-1 w-full text-black italic' - optEl.textContent = 'Item 1' - contentEl.appendChild(optEl) + optEl.className += ' text-black'; } - - } else if (field.type === 'button') { - contentEl.className = 'field-content w-full h-full flex items-center justify-center bg-gray-200 text-sm font-semibold' - contentEl.style.color = field.textColor || '#000000' - contentEl.textContent = field.label || 'Button' - } else if (field.type === 'signature') { - contentEl.className = 'w-full h-full flex items-center justify-center bg-gray-50 text-gray-400' - contentEl.innerHTML = '
Sign Here
' - setTimeout(() => (window as any).lucide?.createIcons(), 0) - } else if (field.type === 'date') { - contentEl.className = 'w-full h-full flex items-center justify-center bg-white text-gray-600 border border-gray-300' - contentEl.innerHTML = `
${field.dateFormat || 'mm/dd/yyyy'}
` - setTimeout(() => (window as any).lucide?.createIcons(), 0) - } else if (field.type === 'image') { - contentEl.className = 'w-full h-full flex items-center justify-center bg-gray-100 text-gray-500 border border-gray-300' - contentEl.innerHTML = `
${field.label || 'Click to Upload Image'}
` - setTimeout(() => (window as any).lucide?.createIcons(), 0) + contentEl.appendChild(optEl); + }); + } else { + // Empty state + const optEl = document.createElement('div'); + optEl.className = 'px-1 w-full text-black italic'; + optEl.textContent = 'Item 1'; + contentEl.appendChild(optEl); } - - fieldContainer.appendChild(contentEl) - fieldWrapper.appendChild(label) - fieldWrapper.appendChild(fieldContainer) - - // Click to select - fieldWrapper.addEventListener('click', (e) => { - e.stopPropagation() - selectField(field) - }) - - // Drag to move - fieldWrapper.addEventListener('mousedown', (e) => { - // Don't start drag if clicking on a resize handle - if ((e.target as HTMLElement).classList.contains('resize-handle')) { - return - } - draggedElement = fieldWrapper - const rect = canvas.getBoundingClientRect() - offsetX = e.clientX - rect.left - field.x - offsetY = e.clientY - rect.top - field.y - selectField(field) - if (gridEnabled) renderGrid() - e.preventDefault() - }) - - // Touch events for moving fields - let touchMoveStarted = false - fieldWrapper.addEventListener('touchstart', (e) => { - if ((e.target as HTMLElement).classList.contains('resize-handle')) { - return - } - touchMoveStarted = false - const touch = e.touches[0] - const rect = canvas.getBoundingClientRect() - offsetX = touch.clientX - rect.left - field.x - offsetY = touch.clientY - rect.top - field.y - selectField(field) - }, { passive: true }) - - fieldWrapper.addEventListener('touchmove', (e) => { - e.preventDefault() - touchMoveStarted = true - const touch = e.touches[0] - const rect = canvas.getBoundingClientRect() - let newX = touch.clientX - rect.left - offsetX - let newY = touch.clientY - rect.top - offsetY - - newX = Math.max(0, Math.min(newX, rect.width - fieldWrapper.offsetWidth)) - newY = Math.max(0, Math.min(newY, rect.height - fieldWrapper.offsetHeight)) - - fieldWrapper.style.left = newX + 'px' - fieldWrapper.style.top = newY + 'px' - - field.x = newX - field.y = newY - }) - - fieldWrapper.addEventListener('touchend', () => { - touchMoveStarted = false - }) - - // Add resize handles to the container - hidden by default - const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w'] - handles.forEach((pos) => { - const handle = document.createElement('div') - handle.className = `absolute w-2.5 h-2.5 bg-white border border-indigo-600 z-10 cursor-${pos}-resize resize-handle hidden` // Added hidden class - const positions: Record = { - nw: 'top-0 left-0 -translate-x-1/2 -translate-y-1/2', - ne: 'top-0 right-0 translate-x-1/2 -translate-y-1/2', - sw: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2', - se: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2', - n: 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2', - s: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2', - e: 'top-1/2 right-0 translate-x-1/2 -translate-y-1/2', - w: 'top-1/2 left-0 -translate-x-1/2 -translate-y-1/2', - } - handle.className += ` ${positions[pos]}` - handle.dataset.position = pos - - handle.addEventListener('mousedown', (e) => { - e.stopPropagation() - startResize(e, field, pos) - }) - - // Touch events for resize handles - handle.addEventListener('touchstart', (e) => { - e.stopPropagation() - e.preventDefault() - const touch = e.touches[0] - // Create a synthetic mouse event for startResize - const syntheticEvent = { - clientX: touch.clientX, - clientY: touch.clientY, - preventDefault: () => { } - } as MouseEvent - startResize(syntheticEvent, field, pos) - }) - - fieldContainer.appendChild(handle) - }) - - canvas.appendChild(fieldWrapper) + } else if (field.type === 'button') { + contentEl.className = + 'field-content w-full h-full flex items-center justify-center bg-gray-200 text-sm font-semibold'; + contentEl.style.color = field.textColor || '#000000'; + contentEl.textContent = field.label || 'Button'; + } else if (field.type === 'signature') { + contentEl.className = + 'w-full h-full flex items-center justify-center bg-gray-50 text-gray-400'; + contentEl.innerHTML = + '
Sign Here
'; + setTimeout(() => (window as any).lucide?.createIcons(), 0); + } else if (field.type === 'date') { + contentEl.className = + 'w-full h-full flex items-center justify-center bg-white text-gray-600 border border-gray-300'; + contentEl.innerHTML = `
${field.dateFormat || 'mm/dd/yyyy'}
`; + setTimeout(() => (window as any).lucide?.createIcons(), 0); + } else if (field.type === 'image') { + contentEl.className = + 'w-full h-full flex items-center justify-center bg-gray-100 text-gray-500 border border-gray-300'; + contentEl.innerHTML = `
${field.label || 'Click to Upload Image'}
`; + setTimeout(() => (window as any).lucide?.createIcons(), 0); + } + + fieldContainer.appendChild(contentEl); + fieldWrapper.appendChild(label); + fieldWrapper.appendChild(fieldContainer); + + // Click to select + fieldWrapper.addEventListener('click', (e) => { + e.stopPropagation(); + selectField(field); + }); + + // Drag to move + fieldWrapper.addEventListener('mousedown', (e) => { + // Don't start drag if clicking on a resize handle + if ((e.target as HTMLElement).classList.contains('resize-handle')) { + return; + } + draggedElement = fieldWrapper; + const rect = canvas.getBoundingClientRect(); + offsetX = e.clientX - rect.left - field.x; + offsetY = e.clientY - rect.top - field.y; + selectField(field); + if (gridEnabled) renderGrid(); + e.preventDefault(); + }); + + // Touch events for moving fields + let touchMoveStarted = false; + fieldWrapper.addEventListener( + 'touchstart', + (e) => { + if ((e.target as HTMLElement).classList.contains('resize-handle')) { + return; + } + touchMoveStarted = false; + const touch = e.touches[0]; + const rect = canvas.getBoundingClientRect(); + offsetX = touch.clientX - rect.left - field.x; + offsetY = touch.clientY - rect.top - field.y; + selectField(field); + }, + { passive: true } + ); + + fieldWrapper.addEventListener('touchmove', (e) => { + e.preventDefault(); + touchMoveStarted = true; + const touch = e.touches[0]; + const rect = canvas.getBoundingClientRect(); + let newX = touch.clientX - rect.left - offsetX; + let newY = touch.clientY - rect.top - offsetY; + + newX = Math.max(0, Math.min(newX, rect.width - fieldWrapper.offsetWidth)); + newY = Math.max(0, Math.min(newY, rect.height - fieldWrapper.offsetHeight)); + + fieldWrapper.style.left = newX + 'px'; + fieldWrapper.style.top = newY + 'px'; + + field.x = newX; + field.y = newY; + }); + + fieldWrapper.addEventListener('touchend', () => { + touchMoveStarted = false; + }); + + // Add resize handles to the container - hidden by default + const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w']; + handles.forEach((pos) => { + const handle = document.createElement('div'); + handle.className = `absolute w-2.5 h-2.5 bg-white border border-indigo-600 z-10 cursor-${pos}-resize resize-handle hidden`; // Added hidden class + const positions: Record = { + nw: 'top-0 left-0 -translate-x-1/2 -translate-y-1/2', + ne: 'top-0 right-0 translate-x-1/2 -translate-y-1/2', + sw: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2', + se: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2', + n: 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2', + s: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2', + e: 'top-1/2 right-0 translate-x-1/2 -translate-y-1/2', + w: 'top-1/2 left-0 -translate-x-1/2 -translate-y-1/2', + }; + handle.className += ` ${positions[pos]}`; + handle.dataset.position = pos; + + handle.addEventListener('mousedown', (e) => { + e.stopPropagation(); + startResize(e, field, pos); + }); + + // Touch events for resize handles + handle.addEventListener('touchstart', (e) => { + e.stopPropagation(); + e.preventDefault(); + const touch = e.touches[0]; + // Create a synthetic mouse event for startResize + const syntheticEvent = { + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {}, + } as MouseEvent; + startResize(syntheticEvent, field, pos); + }); + + fieldContainer.appendChild(handle); + }); + + canvas.appendChild(fieldWrapper); } function startResize(e: MouseEvent, field: FormField, pos: string): void { - resizing = true - resizeField = field - resizePos = pos - startX = e.clientX - startY = e.clientY - startWidth = field.width - startHeight = field.height - startLeft = field.x - startTop = field.y - e.preventDefault() + resizing = true; + resizeField = field; + resizePos = pos; + startX = e.clientX; + startY = e.clientY; + startWidth = field.width; + startHeight = field.height; + startLeft = field.x; + startTop = field.y; + e.preventDefault(); } // Mouse move for dragging and resizing document.addEventListener('mousemove', (e) => { - if (draggedElement && !resizing) { - const rect = canvas.getBoundingClientRect() - let newX = e.clientX - rect.left - offsetX - let newY = e.clientY - rect.top - offsetY - - newX = Math.max(0, Math.min(newX, rect.width - draggedElement.offsetWidth)) - newY = Math.max(0, Math.min(newY, rect.height - draggedElement.offsetHeight)) - - draggedElement.style.left = newX + 'px' - draggedElement.style.top = newY + 'px' - - const field = fields.find((f) => f.id === draggedElement!.id) - if (field) { - field.x = newX - field.y = newY - } - } else if (resizing && resizeField) { - const dx = e.clientX - startX - const dy = e.clientY - startY - const fieldWrapper = document.getElementById(resizeField.id) + if (draggedElement && !resizing) { + const rect = canvas.getBoundingClientRect(); + let newX = e.clientX - rect.left - offsetX; + let newY = e.clientY - rect.top - offsetY; + + newX = Math.max(0, Math.min(newX, rect.width - draggedElement.offsetWidth)); + newY = Math.max( + 0, + Math.min(newY, rect.height - draggedElement.offsetHeight) + ); + + draggedElement.style.left = newX + 'px'; + draggedElement.style.top = newY + 'px'; + + const field = fields.find((f) => f.id === draggedElement!.id); + if (field) { + field.x = newX; + field.y = newY; + } + } else if (resizing && resizeField) { + const dx = e.clientX - startX; + const dy = e.clientY - startY; + const fieldWrapper = document.getElementById(resizeField.id); - if (resizePos!.includes('e')) { - resizeField.width = Math.max(50, startWidth + dx) - } - if (resizePos!.includes('w')) { - const newWidth = Math.max(50, startWidth - dx) - const widthDiff = startWidth - newWidth - resizeField.width = newWidth - resizeField.x = startLeft + widthDiff - } - if (resizePos!.includes('s')) { - resizeField.height = Math.max(20, startHeight + dy) - } - if (resizePos!.includes('n')) { - const newHeight = Math.max(20, startHeight - dy) - const heightDiff = startHeight - newHeight - resizeField.height = newHeight - resizeField.y = startTop + heightDiff - } + if (resizePos!.includes('e')) { + resizeField.width = Math.max(50, startWidth + dx); + } + if (resizePos!.includes('w')) { + const newWidth = Math.max(50, startWidth - dx); + const widthDiff = startWidth - newWidth; + resizeField.width = newWidth; + resizeField.x = startLeft + widthDiff; + } + if (resizePos!.includes('s')) { + resizeField.height = Math.max(20, startHeight + dy); + } + if (resizePos!.includes('n')) { + const newHeight = Math.max(20, startHeight - dy); + const heightDiff = startHeight - newHeight; + resizeField.height = newHeight; + resizeField.y = startTop + heightDiff; + } - if (fieldWrapper) { - const container = fieldWrapper.querySelector('.field-container') as HTMLElement - fieldWrapper.style.width = resizeField.width + 'px' - fieldWrapper.style.left = resizeField.x + 'px' - fieldWrapper.style.top = resizeField.y + 'px' - if (container) { - container.style.height = resizeField.height + 'px' - } - // Update combing visuals on resize - if (resizeField.combCells > 0) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) { - textEl.style.letterSpacing = `calc(${resizeField.width / resizeField.combCells}px - 1ch)` - textEl.style.paddingLeft = `calc((${resizeField.width / resizeField.combCells}px - 1ch) / 2)` - } - } + if (fieldWrapper) { + const container = fieldWrapper.querySelector( + '.field-container' + ) as HTMLElement; + fieldWrapper.style.width = resizeField.width + 'px'; + fieldWrapper.style.left = resizeField.x + 'px'; + fieldWrapper.style.top = resizeField.y + 'px'; + if (container) { + container.style.height = resizeField.height + 'px'; + } + // Update combing visuals on resize + if (resizeField.combCells > 0) { + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) { + textEl.style.letterSpacing = `calc(${resizeField.width / resizeField.combCells}px - 1ch)`; + textEl.style.paddingLeft = `calc((${resizeField.width / resizeField.combCells}px - 1ch) / 2)`; } + } } -}) + } +}); document.addEventListener('mouseup', () => { - draggedElement = null - resizing = false - resizeField = null - if (!gridAlwaysVisible) removeGrid() -}) - -document.addEventListener('touchmove', (e) => { - const touch = e.touches[0] + draggedElement = null; + resizing = false; + resizeField = null; + if (!gridAlwaysVisible) removeGrid(); +}); + +document.addEventListener( + 'touchmove', + (e) => { + const touch = e.touches[0]; if (resizing && resizeField) { - const dx = touch.clientX - startX - const dy = touch.clientY - startY - const fieldWrapper = document.getElementById(resizeField.id) - - if (resizePos!.includes('e')) { - resizeField.width = Math.max(50, startWidth + dx) - } - if (resizePos!.includes('w')) { - const newWidth = Math.max(50, startWidth - dx) - const widthDiff = startWidth - newWidth - resizeField.width = newWidth - resizeField.x = startLeft + widthDiff - } - if (resizePos!.includes('s')) { - resizeField.height = Math.max(20, startHeight + dy) - } - if (resizePos!.includes('n')) { - const newHeight = Math.max(20, startHeight - dy) - const heightDiff = startHeight - newHeight - resizeField.height = newHeight - resizeField.y = startTop + heightDiff + const dx = touch.clientX - startX; + const dy = touch.clientY - startY; + const fieldWrapper = document.getElementById(resizeField.id); + + if (resizePos!.includes('e')) { + resizeField.width = Math.max(50, startWidth + dx); + } + if (resizePos!.includes('w')) { + const newWidth = Math.max(50, startWidth - dx); + const widthDiff = startWidth - newWidth; + resizeField.width = newWidth; + resizeField.x = startLeft + widthDiff; + } + if (resizePos!.includes('s')) { + resizeField.height = Math.max(20, startHeight + dy); + } + if (resizePos!.includes('n')) { + const newHeight = Math.max(20, startHeight - dy); + const heightDiff = startHeight - newHeight; + resizeField.height = newHeight; + resizeField.y = startTop + heightDiff; + } + + if (fieldWrapper) { + const container = fieldWrapper.querySelector( + '.field-container' + ) as HTMLElement; + fieldWrapper.style.width = resizeField.width + 'px'; + fieldWrapper.style.left = resizeField.x + 'px'; + fieldWrapper.style.top = resizeField.y + 'px'; + if (container) { + container.style.height = resizeField.height + 'px'; } - - if (fieldWrapper) { - const container = fieldWrapper.querySelector('.field-container') as HTMLElement - fieldWrapper.style.width = resizeField.width + 'px' - fieldWrapper.style.left = resizeField.x + 'px' - fieldWrapper.style.top = resizeField.y + 'px' - if (container) { - container.style.height = resizeField.height + 'px' - } - if (resizeField.combCells > 0) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) { - textEl.style.letterSpacing = `calc(${resizeField.width / resizeField.combCells}px - 1ch)` - textEl.style.paddingLeft = `calc((${resizeField.width / resizeField.combCells}px - 1ch) / 2)` - } - } + if (resizeField.combCells > 0) { + const textEl = fieldWrapper.querySelector( + '.field-text' + ) as HTMLElement; + if (textEl) { + textEl.style.letterSpacing = `calc(${resizeField.width / resizeField.combCells}px - 1ch)`; + textEl.style.paddingLeft = `calc((${resizeField.width / resizeField.combCells}px - 1ch) / 2)`; + } } + } } -}, { passive: false }) + }, + { passive: false } +); document.addEventListener('touchend', () => { - resizing = false - resizeField = null -}) - - + resizing = false; + resizeField = null; +}); // Select field function selectField(field: FormField): void { - deselectAll() - selectedField = field - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const container = fieldWrapper.querySelector('.field-container') as HTMLElement - const label = fieldWrapper.querySelector('.field-label') as HTMLElement - const handles = fieldWrapper.querySelectorAll('.resize-handle') - - if (container) { - // Remove hover classes and add selected classes - container.classList.remove('border-indigo-200', 'group-hover:border-dashed', 'group-hover:border-indigo-300') - container.classList.add('border-dashed', 'border-indigo-500', 'bg-indigo-50') - } - - if (label) { - label.classList.remove('opacity-0', 'group-hover:opacity-100') - label.classList.add('opacity-100') - } + deselectAll(); + selectedField = field; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const container = fieldWrapper.querySelector( + '.field-container' + ) as HTMLElement; + const label = fieldWrapper.querySelector('.field-label') as HTMLElement; + const handles = fieldWrapper.querySelectorAll('.resize-handle'); + + if (container) { + // Remove hover classes and add selected classes + container.classList.remove( + 'border-indigo-200', + 'group-hover:border-dashed', + 'group-hover:border-indigo-300' + ); + container.classList.add( + 'border-dashed', + 'border-indigo-500', + 'bg-indigo-50' + ); + } - handles.forEach(handle => { - handle.classList.remove('hidden') - }) + if (label) { + label.classList.remove('opacity-0', 'group-hover:opacity-100'); + label.classList.add('opacity-100'); } - showProperties(field) + + handles.forEach((handle) => { + handle.classList.remove('hidden'); + }); + } + showProperties(field); } // Deselect all function deselectAll(): void { - if (selectedField) { - const fieldWrapper = document.getElementById(selectedField.id) - if (fieldWrapper) { - const container = fieldWrapper.querySelector('.field-container') as HTMLElement - const label = fieldWrapper.querySelector('.field-label') as HTMLElement - const handles = fieldWrapper.querySelectorAll('.resize-handle') - - if (container) { - // Revert to default/hover state - container.classList.remove('border-dashed', 'border-indigo-500', 'bg-indigo-50') - container.classList.add('border-indigo-200', 'group-hover:border-dashed', 'group-hover:border-indigo-300') - } - - if (label) { - label.classList.remove('opacity-100') - label.classList.add('opacity-0', 'group-hover:opacity-100') - } - - handles.forEach(handle => { - handle.classList.add('hidden') - }) - } - selectedField = null + if (selectedField) { + const fieldWrapper = document.getElementById(selectedField.id); + if (fieldWrapper) { + const container = fieldWrapper.querySelector( + '.field-container' + ) as HTMLElement; + const label = fieldWrapper.querySelector('.field-label') as HTMLElement; + const handles = fieldWrapper.querySelectorAll('.resize-handle'); + + if (container) { + // Revert to default/hover state + container.classList.remove( + 'border-dashed', + 'border-indigo-500', + 'bg-indigo-50' + ); + container.classList.add( + 'border-indigo-200', + 'group-hover:border-dashed', + 'group-hover:border-indigo-300' + ); + } + + if (label) { + label.classList.remove('opacity-100'); + label.classList.add('opacity-0', 'group-hover:opacity-100'); + } + + handles.forEach((handle) => { + handle.classList.add('hidden'); + }); } - hideProperties() + selectedField = null; + } + hideProperties(); } // Show properties panel function showProperties(field: FormField): void { - let specificProps = '' + let specificProps = ''; - if (field.type === 'text') { - specificProps = ` + if (field.type === 'text') { + specificProps = `
0 ? `maxlength="${field.combCells}"` : field.maxLength > 0 ? `maxlength="${field.maxLength}"` : ''} class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500"> @@ -809,18 +947,18 @@ function showProperties(field: FormField): void {
- ` - } else if (field.type === 'checkbox') { - specificProps = ` + `; + } else if (field.type === 'checkbox') { + specificProps = `
- ` - } else if (field.type === 'radio') { - specificProps = ` + `; + } else if (field.type === 'radio') { + specificProps = `
@@ -835,9 +973,9 @@ function showProperties(field: FormField): void {
- ` - } else if (field.type === 'dropdown' || field.type === 'optionlist') { - specificProps = ` + `; + } else if (field.type === 'dropdown' || field.type === 'optionlist') { + specificProps = `
@@ -846,15 +984,15 @@ function showProperties(field: FormField): void {
To actually fill or change the options, use our PDF Form Filler tool.
- ` - } else if (field.type === 'button') { - specificProps = ` + `; + } else if (field.type === 'button') { + specificProps = `
@@ -883,7 +1021,13 @@ function showProperties(field: FormField): void {
@@ -895,34 +1039,73 @@ function showProperties(field: FormField): void {
- ` - } else if (field.type === 'signature') { - specificProps = ` + `; + } else if (field.type === 'signature') { + specificProps = `
Signature fields are AcroForm signature fields and would only be visible in an advanced PDF viewer.
- ` - } else if (field.type === 'date') { - const formats = ['mm/dd/yyyy', 'dd/mm/yyyy', 'mm/yy', 'dd/mm/yy', 'yyyy/mm/dd', 'mmm d, yyyy', 'd-mmm-yy', 'yy-mm-dd'] - specificProps = ` + `; + } else if (field.type === 'date') { + const formats = [ + 'm/d', + 'm/d/yy', + 'm/d/yyyy', + 'mm/dd/yy', + 'mm/dd/yyyy', + 'mm/yy', + 'mm/yyyy', + 'd-mmm', + 'd-mmm-yy', + 'd-mmm-yyyy', + 'dd-mmm-yy', + 'dd-mmm-yyyy', + 'yy-mm-dd', + 'yyyy-mm-dd', + 'mmm-yy', + 'mmm-yyyy', + 'mmm d, yyyy', + 'mmmm-yy', + 'mmmm-yyyy', + 'mmmm d, yyyy', + 'dd/mm/yy', + 'dd/mm/yyyy', + 'yyyy/mm/dd', + 'dd.mm.yy', + 'dd.mm.yyyy', + 'm/d/yy h:MM tt', + 'm/d/yyyy h:MM tt', + 'm/d/yy HH:MM', + 'm/d/yyyy HH:MM', + 'yyyy-mm', + 'yyyy', + ]; + const isCustom = !formats.includes(field.dateFormat || 'mm/dd/yyyy'); + specificProps = `
-
- The selected format will be enforced when the user types or picks a date. +
+ + +
+
+ Example of current format: +
-

- - Browser Note: Firefox and Chrome may show their native date picker format during selection. The correct format will apply when you finish entering the date. This is normal browser behavior and not an issue. +

+ + Browser Note: Firefox and Chrome may show their native date picker format during selection. The correct format will apply when you finish entering the date.

- ` - } else if (field.type === 'image') { - specificProps = ` + `; + } else if (field.type === 'image') { + specificProps = `
@@ -930,27 +1113,47 @@ function showProperties(field: FormField): void {
Clicking this field in the PDF will open a file picker to upload an image.
- ` - } + `; + } - propertiesPanel.innerHTML = ` + propertiesPanel.innerHTML = `
- ${field.type === 'radio' && (existingRadioGroups.size > 0 || fields.some(f => f.type === 'radio' && f.id !== field.id)) ? ` + ${ + field.type === 'radio' && + (existingRadioGroups.size > 0 || + fields.some((f) => f.type === 'radio' && f.id !== field.id)) + ? `

Select to add this button to an existing group

- ` : ''} + ` + : '' + } ${specificProps}
@@ -976,1113 +1179,1337 @@ function showProperties(field: FormField): void { Delete Field
- ` - - // Common listeners - const propName = document.getElementById('propName') as HTMLInputElement - const nameError = document.getElementById('nameError') as HTMLDivElement - const propTooltip = document.getElementById('propTooltip') as HTMLInputElement - const propRequired = document.getElementById('propRequired') as HTMLInputElement - const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement - const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement - - const validateName = (newName: string): boolean => { - if (!newName) { - nameError.textContent = 'Field name cannot be empty' - nameError.classList.remove('hidden') - propName.classList.add('border-red-500') - return false - } - - if (field.type === 'radio') { - nameError.classList.add('hidden') - propName.classList.remove('border-red-500') - return true - } + `; + + // Common listeners + const propName = document.getElementById('propName') as HTMLInputElement; + const nameError = document.getElementById('nameError') as HTMLDivElement; + const propTooltip = document.getElementById( + 'propTooltip' + ) as HTMLInputElement; + const propRequired = document.getElementById( + 'propRequired' + ) as HTMLInputElement; + const propReadOnly = document.getElementById( + 'propReadOnly' + ) as HTMLInputElement; + const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement; + + const validateName = (newName: string): boolean => { + if (!newName) { + nameError.textContent = 'Field name cannot be empty'; + nameError.classList.remove('hidden'); + propName.classList.add('border-red-500'); + return false; + } - const isDuplicateInFields = fields.some(f => f.id !== field.id && f.name === newName) - const isDuplicateInPdf = existingFieldNames.has(newName) + if (field.type === 'radio') { + nameError.classList.add('hidden'); + propName.classList.remove('border-red-500'); + return true; + } - if (isDuplicateInFields || isDuplicateInPdf) { - nameError.textContent = `Field name "${newName}" already exists in this ${isDuplicateInPdf ? 'PDF' : 'form'}. Please try using a unique name.` - nameError.classList.remove('hidden') - propName.classList.add('border-red-500') - return false - } + const isDuplicateInFields = fields.some( + (f) => f.id !== field.id && f.name === newName + ); + const isDuplicateInPdf = existingFieldNames.has(newName); - nameError.classList.add('hidden') - propName.classList.remove('border-red-500') - return true + if (isDuplicateInFields || isDuplicateInPdf) { + nameError.textContent = `Field name "${newName}" already exists in this ${isDuplicateInPdf ? 'PDF' : 'form'}. Please try using a unique name.`; + nameError.classList.remove('hidden'); + propName.classList.add('border-red-500'); + return false; } - propName.addEventListener('input', (e) => { - const newName = (e.target as HTMLInputElement).value.trim() - validateName(newName) - }) + nameError.classList.add('hidden'); + propName.classList.remove('border-red-500'); + return true; + }; - propName.addEventListener('change', (e) => { - const newName = (e.target as HTMLInputElement).value.trim() + propName.addEventListener('input', (e) => { + const newName = (e.target as HTMLInputElement).value.trim(); + validateName(newName); + }); - if (!validateName(newName)) { - (e.target as HTMLInputElement).value = field.name - return - } + propName.addEventListener('change', (e) => { + const newName = (e.target as HTMLInputElement).value.trim(); - field.name = newName - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const label = fieldWrapper.querySelector('.field-label') as HTMLElement - if (label) label.textContent = field.name - } - }) - - propTooltip.addEventListener('input', (e) => { - field.tooltip = (e.target as HTMLInputElement).value - }) + if (!validateName(newName)) { + (e.target as HTMLInputElement).value = field.name; + return; + } - if (field.type === 'radio') { - const existingGroupsSelect = document.getElementById('existingGroups') as HTMLSelectElement - if (existingGroupsSelect) { - existingGroupsSelect.addEventListener('change', (e) => { - const selectedGroup = (e.target as HTMLSelectElement).value - if (selectedGroup) { - propName.value = selectedGroup - field.name = selectedGroup - validateName(selectedGroup) - - // Update field label - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const label = fieldWrapper.querySelector('.field-label') as HTMLElement - if (label) label.textContent = field.name - } - } - }) + field.name = newName; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const label = fieldWrapper.querySelector('.field-label') as HTMLElement; + if (label) label.textContent = field.name; + } + }); + + propTooltip.addEventListener('input', (e) => { + field.tooltip = (e.target as HTMLInputElement).value; + }); + + if (field.type === 'radio') { + const existingGroupsSelect = document.getElementById( + 'existingGroups' + ) as HTMLSelectElement; + if (existingGroupsSelect) { + existingGroupsSelect.addEventListener('change', (e) => { + const selectedGroup = (e.target as HTMLSelectElement).value; + if (selectedGroup) { + propName.value = selectedGroup; + field.name = selectedGroup; + validateName(selectedGroup); + + // Update field label + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const label = fieldWrapper.querySelector( + '.field-label' + ) as HTMLElement; + if (label) label.textContent = field.name; + } } + }); } + } + + propRequired.addEventListener('change', (e) => { + field.required = (e.target as HTMLInputElement).checked; + }); + + propReadOnly.addEventListener('change', (e) => { + field.readOnly = (e.target as HTMLInputElement).checked; + }); + + const propBorderColor = document.getElementById( + 'propBorderColor' + ) as HTMLInputElement; + const propHideBorder = document.getElementById( + 'propHideBorder' + ) as HTMLInputElement; + + propBorderColor.addEventListener('input', (e) => { + field.borderColor = (e.target as HTMLInputElement).value; + }); + + propHideBorder.addEventListener('change', (e) => { + field.hideBorder = (e.target as HTMLInputElement).checked; + }); + + deleteBtn.addEventListener('click', () => { + deleteField(field); + }); + + // Specific listeners + if (field.type === 'text') { + const propValue = document.getElementById('propValue') as HTMLInputElement; + const propMaxLength = document.getElementById( + 'propMaxLength' + ) as HTMLInputElement; + const propComb = document.getElementById('propComb') as HTMLInputElement; + const propFontSize = document.getElementById( + 'propFontSize' + ) as HTMLInputElement; + const propTextColor = document.getElementById( + 'propTextColor' + ) as HTMLInputElement; + const propAlignment = document.getElementById( + 'propAlignment' + ) as HTMLSelectElement; + + propValue.addEventListener('input', (e) => { + field.defaultValue = (e.target as HTMLInputElement).value; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) textEl.textContent = field.defaultValue; + } + }); + + propMaxLength.addEventListener('input', (e) => { + const val = parseInt((e.target as HTMLInputElement).value); + field.maxLength = isNaN(val) ? 0 : Math.max(0, val); + if (field.maxLength > 0) { + propValue.maxLength = field.maxLength; + if (field.defaultValue.length > field.maxLength) { + field.defaultValue = field.defaultValue.substring(0, field.maxLength); + propValue.value = field.defaultValue; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector( + '.field-text' + ) as HTMLElement; + if (textEl) textEl.textContent = field.defaultValue; + } + } + } else { + propValue.removeAttribute('maxLength'); + } + }); + + propComb.addEventListener('input', (e) => { + const val = parseInt((e.target as HTMLInputElement).value); + field.combCells = isNaN(val) ? 0 : Math.max(0, val); + + if (field.combCells > 0) { + propValue.maxLength = field.combCells; + propMaxLength.value = field.combCells.toString(); + propMaxLength.disabled = true; + field.maxLength = field.combCells; + + if (field.defaultValue.length > field.combCells) { + field.defaultValue = field.defaultValue.substring(0, field.combCells); + propValue.value = field.defaultValue; + } + } else { + propMaxLength.disabled = false; + propValue.removeAttribute('maxLength'); + if (field.maxLength > 0) { + propValue.maxLength = field.maxLength; + } + } + + // Re-render field visual only, NOT the properties panel + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + // Update text content + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) { + textEl.textContent = field.defaultValue; + if (field.combCells > 0) { + textEl.style.backgroundImage = `repeating-linear-gradient(90deg, transparent, transparent calc((100% / ${field.combCells}) - 1px), #e5e7eb calc((100% / ${field.combCells}) - 1px), #e5e7eb calc(100% / ${field.combCells}))`; + textEl.style.fontFamily = 'monospace'; + textEl.style.letterSpacing = `calc(${field.width / field.combCells}px - 1ch)`; + textEl.style.paddingLeft = `calc((${field.width / field.combCells}px - 1ch) / 2)`; + textEl.style.overflow = 'hidden'; + textEl.style.textAlign = 'left'; + textEl.style.justifyContent = 'flex-start'; + } else { + textEl.style.backgroundImage = 'none'; + textEl.style.fontFamily = 'inherit'; + textEl.style.letterSpacing = 'normal'; + textEl.style.textAlign = field.alignment; + textEl.style.justifyContent = + field.alignment === 'left' + ? 'flex-start' + : field.alignment === 'right' + ? 'flex-end' + : 'center'; + } + } + } + }); + + propFontSize.addEventListener('input', (e) => { + field.fontSize = parseInt((e.target as HTMLInputElement).value); + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) textEl.style.fontSize = field.fontSize + 'px'; + } + }); + + propTextColor.addEventListener('input', (e) => { + field.textColor = (e.target as HTMLInputElement).value; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) textEl.style.color = field.textColor; + } + }); + + propAlignment.addEventListener('change', (e) => { + field.alignment = (e.target as HTMLSelectElement).value as + | 'left' + | 'center' + | 'right'; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement; + if (textEl) { + textEl.style.textAlign = field.alignment; + textEl.style.justifyContent = + field.alignment === 'left' + ? 'flex-start' + : field.alignment === 'right' + ? 'flex-end' + : 'center'; + } + } + }); + + const propMultilineBtn = document.getElementById( + 'propMultilineBtn' + ) as HTMLButtonElement; + if (propMultilineBtn) { + propMultilineBtn.addEventListener('click', () => { + field.multiline = !field.multiline; + + // Update Toggle Button UI + const span = propMultilineBtn.querySelector('span'); + if (field.multiline) { + propMultilineBtn.classList.remove('bg-gray-500'); + propMultilineBtn.classList.add('bg-indigo-600'); + span?.classList.remove('translate-x-0'); + span?.classList.add('translate-x-6'); + } else { + propMultilineBtn.classList.remove('bg-indigo-600'); + propMultilineBtn.classList.add('bg-gray-500'); + span?.classList.remove('translate-x-6'); + span?.classList.add('translate-x-0'); + } - propRequired.addEventListener('change', (e) => { - field.required = (e.target as HTMLInputElement).checked - }) - - propReadOnly.addEventListener('change', (e) => { - field.readOnly = (e.target as HTMLInputElement).checked - }) - - const propBorderColor = document.getElementById('propBorderColor') as HTMLInputElement - const propHideBorder = document.getElementById('propHideBorder') as HTMLInputElement - - propBorderColor.addEventListener('input', (e) => { - field.borderColor = (e.target as HTMLInputElement).value - }) - - propHideBorder.addEventListener('change', (e) => { - field.hideBorder = (e.target as HTMLInputElement).checked - }) - - deleteBtn.addEventListener('click', () => { - deleteField(field) - }) - - // Specific listeners - if (field.type === 'text') { - const propValue = document.getElementById('propValue') as HTMLInputElement - const propMaxLength = document.getElementById('propMaxLength') as HTMLInputElement - const propComb = document.getElementById('propComb') as HTMLInputElement - const propFontSize = document.getElementById('propFontSize') as HTMLInputElement - const propTextColor = document.getElementById('propTextColor') as HTMLInputElement - const propAlignment = document.getElementById('propAlignment') as HTMLSelectElement - - propValue.addEventListener('input', (e) => { - field.defaultValue = (e.target as HTMLInputElement).value - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) textEl.textContent = field.defaultValue - } - }) - - propMaxLength.addEventListener('input', (e) => { - const val = parseInt((e.target as HTMLInputElement).value) - field.maxLength = isNaN(val) ? 0 : Math.max(0, val) - if (field.maxLength > 0) { - propValue.maxLength = field.maxLength - if (field.defaultValue.length > field.maxLength) { - field.defaultValue = field.defaultValue.substring(0, field.maxLength) - propValue.value = field.defaultValue - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) textEl.textContent = field.defaultValue - } - } - } else { - propValue.removeAttribute('maxLength') - } - }) - - propComb.addEventListener('input', (e) => { - const val = parseInt((e.target as HTMLInputElement).value) - field.combCells = isNaN(val) ? 0 : Math.max(0, val) - - if (field.combCells > 0) { - propValue.maxLength = field.combCells - propMaxLength.value = field.combCells.toString() - propMaxLength.disabled = true - field.maxLength = field.combCells - - if (field.defaultValue.length > field.combCells) { - field.defaultValue = field.defaultValue.substring(0, field.combCells) - propValue.value = field.defaultValue - } + // Update Canvas UI + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textEl = fieldWrapper.querySelector( + '.field-text' + ) as HTMLElement; + if (textEl) { + if (field.multiline) { + textEl.style.whiteSpace = 'pre-wrap'; + textEl.style.alignItems = 'flex-start'; + textEl.style.overflow = 'hidden'; } else { - propMaxLength.disabled = false - propValue.removeAttribute('maxLength') - if (field.maxLength > 0) { - propValue.maxLength = field.maxLength - } - } - - // Re-render field visual only, NOT the properties panel - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - // Update text content - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) { - textEl.textContent = field.defaultValue - if (field.combCells > 0) { - textEl.style.backgroundImage = `repeating-linear-gradient(90deg, transparent, transparent calc((100% / ${field.combCells}) - 1px), #e5e7eb calc((100% / ${field.combCells}) - 1px), #e5e7eb calc(100% / ${field.combCells}))` - textEl.style.fontFamily = 'monospace' - textEl.style.letterSpacing = `calc(${field.width / field.combCells}px - 1ch)` - textEl.style.paddingLeft = `calc((${field.width / field.combCells}px - 1ch) / 2)` - textEl.style.overflow = 'hidden' - textEl.style.textAlign = 'left' - textEl.style.justifyContent = 'flex-start' - } else { - textEl.style.backgroundImage = 'none' - textEl.style.fontFamily = 'inherit' - textEl.style.letterSpacing = 'normal' - textEl.style.textAlign = field.alignment - textEl.style.justifyContent = field.alignment === 'left' ? 'flex-start' : field.alignment === 'right' ? 'flex-end' : 'center' - } - } - } - }) - - propFontSize.addEventListener('input', (e) => { - field.fontSize = parseInt((e.target as HTMLInputElement).value) - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) textEl.style.fontSize = field.fontSize + 'px' - } - }) - - propTextColor.addEventListener('input', (e) => { - field.textColor = (e.target as HTMLInputElement).value - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) textEl.style.color = field.textColor - } - }) - - propAlignment.addEventListener('change', (e) => { - field.alignment = (e.target as HTMLSelectElement).value as 'left' | 'center' | 'right' - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) { - textEl.style.textAlign = field.alignment - textEl.style.justifyContent = field.alignment === 'left' ? 'flex-start' : field.alignment === 'right' ? 'flex-end' : 'center' - } + textEl.style.whiteSpace = 'nowrap'; + textEl.style.alignItems = 'center'; + textEl.style.overflow = 'hidden'; } - }) - - const propMultilineBtn = document.getElementById('propMultilineBtn') as HTMLButtonElement - if (propMultilineBtn) { - propMultilineBtn.addEventListener('click', () => { - field.multiline = !field.multiline - - // Update Toggle Button UI - const span = propMultilineBtn.querySelector('span') - if (field.multiline) { - propMultilineBtn.classList.remove('bg-gray-500') - propMultilineBtn.classList.add('bg-indigo-600') - span?.classList.remove('translate-x-0') - span?.classList.add('translate-x-6') - } else { - propMultilineBtn.classList.remove('bg-indigo-600') - propMultilineBtn.classList.add('bg-gray-500') - span?.classList.remove('translate-x-6') - span?.classList.add('translate-x-0') - } - - // Update Canvas UI - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textEl = fieldWrapper.querySelector('.field-text') as HTMLElement - if (textEl) { - if (field.multiline) { - textEl.style.whiteSpace = 'pre-wrap' - textEl.style.alignItems = 'flex-start' - textEl.style.overflow = 'hidden' - } else { - textEl.style.whiteSpace = 'nowrap' - textEl.style.alignItems = 'center' - textEl.style.overflow = 'hidden' - } - } - } - }) + } } - } else if (field.type === 'checkbox' || field.type === 'radio') { - const propCheckedBtn = document.getElementById('propCheckedBtn') as HTMLButtonElement - - propCheckedBtn.addEventListener('click', () => { - field.checked = !field.checked - - // Update Toggle Button UI - const span = propCheckedBtn.querySelector('span') - if (field.checked) { - propCheckedBtn.classList.remove('bg-gray-500') - propCheckedBtn.classList.add('bg-indigo-600') - span?.classList.remove('translate-x-0') - span?.classList.add('translate-x-6') - } else { - propCheckedBtn.classList.remove('bg-indigo-600') - propCheckedBtn.classList.add('bg-gray-500') - span?.classList.remove('translate-x-6') - span?.classList.add('translate-x-0') - } - - // Update Canvas UI - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const contentEl = fieldWrapper.querySelector('.field-content') as HTMLElement - if (contentEl) { - if (field.type === 'checkbox') { - contentEl.innerHTML = field.checked ? '' : '' - } else { - contentEl.innerHTML = field.checked ? '
' : '' - } - } - } - }) - - if (field.type === 'radio') { - const propGroupName = document.getElementById('propGroupName') as HTMLInputElement - const propExportValue = document.getElementById('propExportValue') as HTMLInputElement - - propGroupName.addEventListener('input', (e) => { - field.groupName = (e.target as HTMLInputElement).value - }) - propExportValue.addEventListener('input', (e) => { - field.exportValue = (e.target as HTMLInputElement).value - }) + }); + } + } else if (field.type === 'checkbox' || field.type === 'radio') { + const propCheckedBtn = document.getElementById( + 'propCheckedBtn' + ) as HTMLButtonElement; + + propCheckedBtn.addEventListener('click', () => { + field.checked = !field.checked; + + // Update Toggle Button UI + const span = propCheckedBtn.querySelector('span'); + if (field.checked) { + propCheckedBtn.classList.remove('bg-gray-500'); + propCheckedBtn.classList.add('bg-indigo-600'); + span?.classList.remove('translate-x-0'); + span?.classList.add('translate-x-6'); + } else { + propCheckedBtn.classList.remove('bg-indigo-600'); + propCheckedBtn.classList.add('bg-gray-500'); + span?.classList.remove('translate-x-6'); + span?.classList.add('translate-x-0'); + } + + // Update Canvas UI + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const contentEl = fieldWrapper.querySelector( + '.field-content' + ) as HTMLElement; + if (contentEl) { + if (field.type === 'checkbox') { + contentEl.innerHTML = field.checked + ? '' + : ''; + } else { + contentEl.innerHTML = field.checked + ? '
' + : ''; + } } - } else if (field.type === 'dropdown' || field.type === 'optionlist') { - const propOptions = document.getElementById('propOptions') as HTMLTextAreaElement - propOptions.addEventListener('input', (e) => { - // We split by newline OR comma for the actual options array - const val = (e.target as HTMLTextAreaElement).value - field.options = val.split(/[\n,]/).map(s => s.trim()).filter(s => s.length > 0) - - const propSelectedOption = document.getElementById('propSelectedOption') as HTMLSelectElement - if (propSelectedOption) { - const currentVal = field.defaultValue - propSelectedOption.innerHTML = '' + - field.options?.map(opt => ``).join('') - - if (currentVal && field.options && !field.options.includes(currentVal)) { - field.defaultValue = '' - propSelectedOption.value = '' - } - } + } + }); - renderField(field) - }) - - const propSelectedOption = document.getElementById('propSelectedOption') as HTMLSelectElement - propSelectedOption.addEventListener('change', (e) => { - field.defaultValue = (e.target as HTMLSelectElement).value - - // Update visual on canvas - renderField(field) - }) - } else if (field.type === 'button') { - const propLabel = document.getElementById('propLabel') as HTMLInputElement - propLabel.addEventListener('input', (e) => { - field.label = (e.target as HTMLInputElement).value - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const contentEl = fieldWrapper.querySelector('.field-content') as HTMLElement - if (contentEl) contentEl.textContent = field.label || 'Button' - } - }) - - const propAction = document.getElementById('propAction') as HTMLSelectElement - const propUrlContainer = document.getElementById('propUrlContainer') as HTMLDivElement - const propJsContainer = document.getElementById('propJsContainer') as HTMLDivElement - const propShowHideContainer = document.getElementById('propShowHideContainer') as HTMLDivElement - - propAction.addEventListener('change', (e) => { - field.action = (e.target as HTMLSelectElement).value as any - - // Show/hide containers - propUrlContainer.classList.add('hidden') - propJsContainer.classList.add('hidden') - propShowHideContainer.classList.add('hidden') - - if (field.action === 'url') { - propUrlContainer.classList.remove('hidden') - } else if (field.action === 'js') { - propJsContainer.classList.remove('hidden') - } else if (field.action === 'showHide') { - propShowHideContainer.classList.remove('hidden') - } - }) - - const propActionUrl = document.getElementById('propActionUrl') as HTMLInputElement - propActionUrl.addEventListener('input', (e) => { - field.actionUrl = (e.target as HTMLInputElement).value - }) - - const propJsScript = document.getElementById('propJsScript') as HTMLTextAreaElement - if (propJsScript) { - propJsScript.addEventListener('input', (e) => { - field.jsScript = (e.target as HTMLTextAreaElement).value - }) + if (field.type === 'radio') { + const propGroupName = document.getElementById( + 'propGroupName' + ) as HTMLInputElement; + const propExportValue = document.getElementById( + 'propExportValue' + ) as HTMLInputElement; + + propGroupName.addEventListener('input', (e) => { + field.groupName = (e.target as HTMLInputElement).value; + }); + propExportValue.addEventListener('input', (e) => { + field.exportValue = (e.target as HTMLInputElement).value; + }); + } + } else if (field.type === 'dropdown' || field.type === 'optionlist') { + const propOptions = document.getElementById( + 'propOptions' + ) as HTMLTextAreaElement; + propOptions.addEventListener('input', (e) => { + // We split by newline OR comma for the actual options array + const val = (e.target as HTMLTextAreaElement).value; + field.options = val + .split(/[\n,]/) + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + const propSelectedOption = document.getElementById( + 'propSelectedOption' + ) as HTMLSelectElement; + if (propSelectedOption) { + const currentVal = field.defaultValue; + propSelectedOption.innerHTML = + '' + + field.options + ?.map( + (opt) => + `` + ) + .join(''); + + if ( + currentVal && + field.options && + !field.options.includes(currentVal) + ) { + field.defaultValue = ''; + propSelectedOption.value = ''; } + } + + renderField(field); + }); + + const propSelectedOption = document.getElementById( + 'propSelectedOption' + ) as HTMLSelectElement; + propSelectedOption.addEventListener('change', (e) => { + field.defaultValue = (e.target as HTMLSelectElement).value; + + // Update visual on canvas + renderField(field); + }); + } else if (field.type === 'button') { + const propLabel = document.getElementById('propLabel') as HTMLInputElement; + propLabel.addEventListener('input', (e) => { + field.label = (e.target as HTMLInputElement).value; + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const contentEl = fieldWrapper.querySelector( + '.field-content' + ) as HTMLElement; + if (contentEl) contentEl.textContent = field.label || 'Button'; + } + }); + + const propAction = document.getElementById( + 'propAction' + ) as HTMLSelectElement; + const propUrlContainer = document.getElementById( + 'propUrlContainer' + ) as HTMLDivElement; + const propJsContainer = document.getElementById( + 'propJsContainer' + ) as HTMLDivElement; + const propShowHideContainer = document.getElementById( + 'propShowHideContainer' + ) as HTMLDivElement; + + propAction.addEventListener('change', (e) => { + field.action = (e.target as HTMLSelectElement).value as any; + + // Show/hide containers + propUrlContainer.classList.add('hidden'); + propJsContainer.classList.add('hidden'); + propShowHideContainer.classList.add('hidden'); + + if (field.action === 'url') { + propUrlContainer.classList.remove('hidden'); + } else if (field.action === 'js') { + propJsContainer.classList.remove('hidden'); + } else if (field.action === 'showHide') { + propShowHideContainer.classList.remove('hidden'); + } + }); + + const propActionUrl = document.getElementById( + 'propActionUrl' + ) as HTMLInputElement; + propActionUrl.addEventListener('input', (e) => { + field.actionUrl = (e.target as HTMLInputElement).value; + }); + + const propJsScript = document.getElementById( + 'propJsScript' + ) as HTMLTextAreaElement; + if (propJsScript) { + propJsScript.addEventListener('input', (e) => { + field.jsScript = (e.target as HTMLTextAreaElement).value; + }); + } - const propTargetField = document.getElementById('propTargetField') as HTMLSelectElement - if (propTargetField) { - propTargetField.addEventListener('change', (e) => { - field.targetFieldName = (e.target as HTMLSelectElement).value - }) - } + const propTargetField = document.getElementById( + 'propTargetField' + ) as HTMLSelectElement; + if (propTargetField) { + propTargetField.addEventListener('change', (e) => { + field.targetFieldName = (e.target as HTMLSelectElement).value; + }); + } - const propVisibilityAction = document.getElementById('propVisibilityAction') as HTMLSelectElement - if (propVisibilityAction) { - propVisibilityAction.addEventListener('change', (e) => { - field.visibilityAction = (e.target as HTMLSelectElement).value as any - }) + const propVisibilityAction = document.getElementById( + 'propVisibilityAction' + ) as HTMLSelectElement; + if (propVisibilityAction) { + propVisibilityAction.addEventListener('change', (e) => { + field.visibilityAction = (e.target as HTMLSelectElement).value as any; + }); + } + } else if (field.type === 'signature') { + // No specific listeners for signature fields yet + } else if (field.type === 'date') { + const propDateFormat = document.getElementById( + 'propDateFormat' + ) as HTMLSelectElement; + const customFormatContainer = document.getElementById( + 'customFormatContainer' + ) as HTMLDivElement; + const propCustomFormat = document.getElementById( + 'propCustomFormat' + ) as HTMLInputElement; + const dateFormatExample = document.getElementById( + 'dateFormatExample' + ) as HTMLSpanElement; + + const formatDateExample = (format: string): string => { + const now = new Date(); + const d = now.getDate(); + const dd = d.toString().padStart(2, '0'); + const m = now.getMonth() + 1; + const mm = m.toString().padStart(2, '0'); + const yy = now.getFullYear().toString().slice(-2); + const yyyy = now.getFullYear().toString(); + const h = now.getHours() % 12 || 12; + const HH = now.getHours().toString().padStart(2, '0'); + const MM = now.getMinutes().toString().padStart(2, '0'); + const tt = now.getHours() >= 12 ? 'PM' : 'AM'; + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + const monthNamesFull = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + const mmm = monthNames[now.getMonth()]; + const mmmm = monthNamesFull[now.getMonth()]; + + return format + .replace(/mmmm/g, mmmm) + .replace(/mmm/g, mmm) + .replace(/mm/g, mm) + .replace(/m/g, m.toString()) + .replace(/dddd/g, dd) + .replace(/dd/g, dd) + .replace(/d/g, d.toString()) + .replace(/yyyy/g, yyyy) + .replace(/yy/g, yy) + .replace(/HH/g, HH) + .replace(/h/g, h.toString()) + .replace(/MM/g, MM) + .replace(/tt/g, tt); + }; + + const updateExample = () => { + if (dateFormatExample) { + dateFormatExample.textContent = formatDateExample( + field.dateFormat || 'mm/dd/yyyy' + ); + } + }; + + updateExample(); + + if (propDateFormat) { + propDateFormat.addEventListener('change', (e) => { + const value = (e.target as HTMLSelectElement).value; + if (value === 'custom') { + customFormatContainer?.classList.remove('hidden'); + if (propCustomFormat && propCustomFormat.value) { + field.dateFormat = propCustomFormat.value; + } + } else { + customFormatContainer?.classList.add('hidden'); + field.dateFormat = value; } - } else if (field.type === 'signature') { - // No specific listeners for signature fields yet - } else if (field.type === 'date') { - const propDateFormat = document.getElementById('propDateFormat') as HTMLSelectElement - if (propDateFormat) { - propDateFormat.addEventListener('change', (e) => { - field.dateFormat = (e.target as HTMLSelectElement).value - // Update canvas preview - const fieldWrapper = document.getElementById(field.id) - if (fieldWrapper) { - const textSpan = fieldWrapper.querySelector('.date-format-text') as HTMLElement - if (textSpan) { - textSpan.textContent = field.dateFormat - } - } - // Re-initialize lucide icons in the properties panel - setTimeout(() => (window as any).lucide?.createIcons(), 0) - }) + updateExample(); + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textSpan = fieldWrapper.querySelector( + '.date-format-text' + ) as HTMLElement; + if (textSpan) { + textSpan.textContent = field.dateFormat; + } } - } else if (field.type === 'image') { - const propLabel = document.getElementById('propLabel') as HTMLInputElement - propLabel.addEventListener('input', (e) => { - field.label = (e.target as HTMLInputElement).value - renderField(field) - }) + setTimeout(() => (window as any).lucide?.createIcons(), 0); + }); } + + if (propCustomFormat) { + propCustomFormat.addEventListener('input', (e) => { + field.dateFormat = (e.target as HTMLInputElement).value || 'mm/dd/yyyy'; + updateExample(); + const fieldWrapper = document.getElementById(field.id); + if (fieldWrapper) { + const textSpan = fieldWrapper.querySelector( + '.date-format-text' + ) as HTMLElement; + if (textSpan) { + textSpan.textContent = field.dateFormat; + } + } + }); + } + } else if (field.type === 'image') { + const propLabel = document.getElementById('propLabel') as HTMLInputElement; + propLabel.addEventListener('input', (e) => { + field.label = (e.target as HTMLInputElement).value; + renderField(field); + }); + } } // Hide properties panel function hideProperties(): void { - propertiesPanel.innerHTML = '

Select a field to edit properties

' + propertiesPanel.innerHTML = + '

Select a field to edit properties

'; } // Delete field function deleteField(field: FormField): void { - const fieldEl = document.getElementById(field.id) - if (fieldEl) { - fieldEl.remove() - } - fields = fields.filter((f) => f.id !== field.id) - deselectAll() - updateFieldCount() + const fieldEl = document.getElementById(field.id); + if (fieldEl) { + fieldEl.remove(); + } + fields = fields.filter((f) => f.id !== field.id); + deselectAll(); + updateFieldCount(); } // Delete key handler document.addEventListener('keydown', (e) => { - if (e.key === 'Delete' && selectedField) { - deleteField(selectedField) - } else if (e.key === 'Escape' && selectedToolType) { - // Cancel tool selection - toolItems.forEach(item => item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600')) - selectedToolType = null - canvas.style.cursor = 'default' - } -}) + if (e.key === 'Delete' && selectedField) { + deleteField(selectedField); + } else if (e.key === 'Escape' && selectedToolType) { + // Cancel tool selection + toolItems.forEach((item) => + item.classList.remove('ring-2', 'ring-indigo-400', 'bg-indigo-600') + ); + selectedToolType = null; + canvas.style.cursor = 'default'; + } +}); // Update field count function updateFieldCount(): void { - fieldCountDisplay.textContent = fields.length.toString() + fieldCountDisplay.textContent = fields.length.toString(); } // Download PDF downloadBtn.addEventListener('click', async () => { - // Check for duplicate field names before generating PDF - const nameCount = new Map() - const duplicates: string[] = [] - const conflictsWithPdf: string[] = [] - - fields.forEach(field => { - const count = nameCount.get(field.name) || 0 - nameCount.set(field.name, count + 1) - - if (existingFieldNames.has(field.name)) { - if (field.type === 'radio' && existingRadioGroups.has(field.name)) { - } else { - conflictsWithPdf.push(field.name) - } - } - }) - - nameCount.forEach((count, name) => { - if (count > 1) { - const fieldsWithName = fields.filter(f => f.name === name) - const allRadio = fieldsWithName.every(f => f.type === 'radio') - - if (!allRadio) { - duplicates.push(name) - } - } - }) - - if (conflictsWithPdf.length > 0) { - const conflictList = [...new Set(conflictsWithPdf)].map(name => `"${name}"`).join(', ') - showModal( - 'Field Name Conflict', - `The following field names already exist in the uploaded PDF: ${conflictList}. Please rename these fields before downloading.`, - 'error' - ) - return + // Check for duplicate field names before generating PDF + const nameCount = new Map(); + const duplicates: string[] = []; + const conflictsWithPdf: string[] = []; + + fields.forEach((field) => { + const count = nameCount.get(field.name) || 0; + nameCount.set(field.name, count + 1); + + if (existingFieldNames.has(field.name)) { + if (field.type === 'radio' && existingRadioGroups.has(field.name)) { + } else { + conflictsWithPdf.push(field.name); + } } + }); - if (duplicates.length > 0) { - const duplicateList = duplicates.map(name => `"${name}"`).join(', ') - showModal( - 'Duplicate Field Names', - `The following field names are used more than once: ${duplicateList}. Please rename these fields to use unique names before downloading.`, - 'error' - ) - return - } + nameCount.forEach((count, name) => { + if (count > 1) { + const fieldsWithName = fields.filter((f) => f.name === name); + const allRadio = fieldsWithName.every((f) => f.type === 'radio'); - if (fields.length === 0) { - alert('Please add at least one field before downloading.') - return + if (!allRadio) { + duplicates.push(name); + } } + }); + + if (conflictsWithPdf.length > 0) { + const conflictList = [...new Set(conflictsWithPdf)] + .map((name) => `"${name}"`) + .join(', '); + showModal( + 'Field Name Conflict', + `The following field names already exist in the uploaded PDF: ${conflictList}. Please rename these fields before downloading.`, + 'error' + ); + return; + } + + if (duplicates.length > 0) { + const duplicateList = duplicates.map((name) => `"${name}"`).join(', '); + showModal( + 'Duplicate Field Names', + `The following field names are used more than once: ${duplicateList}. Please rename these fields to use unique names before downloading.`, + 'error' + ); + return; + } + + if (fields.length === 0) { + alert('Please add at least one field before downloading.'); + return; + } + + if (pages.length === 0) { + alert('No pages found. Please create a blank PDF or upload one.'); + return; + } + + try { + let pdfDoc: PDFDocument; - if (pages.length === 0) { - alert('No pages found. Please create a blank PDF or upload one.') - return - } + if (uploadedPdfDoc) { + pdfDoc = uploadedPdfDoc; + } else { + pdfDoc = await PDFDocument.create(); - try { - let pdfDoc: PDFDocument + for (const pageData of pages) { + pdfDoc.addPage([pageData.width, pageData.height]); + } + } - if (uploadedPdfDoc) { - pdfDoc = uploadedPdfDoc + const form = pdfDoc.getForm(); + + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + + // Set document metadata for accessibility + pdfDoc.setTitle('Fillable Form'); + pdfDoc.setAuthor('BentoPDF'); + pdfDoc.setLanguage('en-US'); + + const radioGroups = new Map(); // Track created radio groups + + for (const field of fields) { + const pageData = pages[field.pageIndex]; + if (!pageData) continue; + + const pdfPage = pdfDoc.getPage(field.pageIndex); + const { height: pageHeight } = pdfPage.getSize(); + + const scaleX = 1 / pdfViewerScale; + const scaleY = 1 / pdfViewerScale; + + const adjustedX = field.x - pdfViewerOffset.x; + const adjustedY = field.y - pdfViewerOffset.y; + + const x = adjustedX * scaleX; + const y = pageHeight - adjustedY * scaleY - field.height * scaleY; + const width = field.width * scaleX; + const height = field.height * scaleY; + + console.log(`Field "${field.name}":`, { + screenPos: { x: field.x, y: field.y }, + adjustedPos: { x: adjustedX, y: adjustedY }, + pdfPos: { x, y, width, height }, + metrics: { offset: pdfViewerOffset, scale: pdfViewerScale }, + }); + + if (field.type === 'text') { + const textField = form.createTextField(field.name); + const rgbColor = hexToRgb(field.textColor); + const borderRgb = hexToRgb(field.borderColor || '#000000'); + + textField.addToPage(pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(1, 1, 1), + textColor: rgb(rgbColor.r, rgbColor.g, rgbColor.b), + }); + + textField.setText(field.defaultValue); + textField.setFontSize(field.fontSize); + + // Set alignment + if (field.alignment === 'center') { + textField.setAlignment(TextAlignment.Center); + } else if (field.alignment === 'right') { + textField.setAlignment(TextAlignment.Right); } else { - pdfDoc = await PDFDocument.create() - - for (const pageData of pages) { - pdfDoc.addPage([pageData.width, pageData.height]) - } + textField.setAlignment(TextAlignment.Left); } - const form = pdfDoc.getForm() - - const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) - - // Set document metadata for accessibility - pdfDoc.setTitle('Fillable Form') - pdfDoc.setAuthor('BentoPDF') - pdfDoc.setLanguage('en-US') - - const radioGroups = new Map() // Track created radio groups - - for (const field of fields) { - const pageData = pages[field.pageIndex] - if (!pageData) continue - - const pdfPage = pdfDoc.getPage(field.pageIndex) - const { height: pageHeight } = pdfPage.getSize() - - const scaleX = 1 / pdfViewerScale - const scaleY = 1 / pdfViewerScale - - const adjustedX = field.x - pdfViewerOffset.x - const adjustedY = field.y - pdfViewerOffset.y - - const x = adjustedX * scaleX - const y = pageHeight - (adjustedY * scaleY) - (field.height * scaleY) - const width = field.width * scaleX - const height = field.height * scaleY - - console.log(`Field "${field.name}":`, { - screenPos: { x: field.x, y: field.y }, - adjustedPos: { x: adjustedX, y: adjustedY }, - pdfPos: { x, y, width, height }, - metrics: { offset: pdfViewerOffset, scale: pdfViewerScale } - }) - - if (field.type === 'text') { - const textField = form.createTextField(field.name) - const rgbColor = hexToRgb(field.textColor) - const borderRgb = hexToRgb(field.borderColor || '#000000') - - textField.addToPage(pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(1, 1, 1), - textColor: rgb(rgbColor.r, rgbColor.g, rgbColor.b), - }) - - textField.setText(field.defaultValue) - textField.setFontSize(field.fontSize) - - // Set alignment - if (field.alignment === 'center') { - textField.setAlignment(TextAlignment.Center) - } else if (field.alignment === 'right') { - textField.setAlignment(TextAlignment.Right) - } else { - textField.setAlignment(TextAlignment.Left) - } - - // Handle combing - if (field.combCells > 0) { - textField.setMaxLength(field.combCells) - textField.enableCombing() - } else if (field.maxLength > 0) { - textField.setMaxLength(field.maxLength) - } - - // Disable multiline to prevent RTL issues (unless explicitly enabled) - if (!field.multiline) { - textField.disableMultiline() - } else { - textField.enableMultiline() - } - - // Common properties - if (field.required) textField.enableRequired() - if (field.readOnly) textField.enableReadOnly() - if (field.tooltip) { - textField.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - - } else if (field.type === 'checkbox') { - const checkBox = form.createCheckBox(field.name) - const borderRgb = hexToRgb(field.borderColor || '#000000') - checkBox.addToPage(pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(1, 1, 1), - }) - if (field.checked) checkBox.check() - if (field.required) checkBox.enableRequired() - if (field.readOnly) checkBox.enableReadOnly() - if (field.tooltip) { - checkBox.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - - } else if (field.type === 'radio') { - const groupName = field.name - let radioGroup - - if (radioGroups.has(groupName)) { - radioGroup = radioGroups.get(groupName) - } else { - const existingField = form.getFieldMaybe(groupName) - - if (existingField) { - radioGroup = existingField - radioGroups.set(groupName, radioGroup) - console.log(`Using existing radio group from PDF: ${groupName}`) - } else { - radioGroup = form.createRadioGroup(groupName) - radioGroups.set(groupName, radioGroup) - console.log(`Created new radio group: ${groupName}`) - } - } - - const borderRgb = hexToRgb(field.borderColor || '#000000') - radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(1, 1, 1), - }) - if (field.checked) radioGroup.select(field.exportValue || 'Yes') - if (field.required) radioGroup.enableRequired() - if (field.readOnly) radioGroup.enableReadOnly() - if (field.tooltip) { - radioGroup.acroField.getWidgets().forEach((widget: any) => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - - } else if (field.type === 'dropdown') { - const dropdown = form.createDropdown(field.name) - const borderRgb = hexToRgb(field.borderColor || '#000000') - dropdown.addToPage(pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(1, 1, 1), // Light blue not supported in standard PDF appearance easily without streams - }) - if (field.options) dropdown.setOptions(field.options) - if (field.defaultValue && field.options?.includes(field.defaultValue)) dropdown.select(field.defaultValue) - else if (field.options && field.options.length > 0) dropdown.select(field.options[0]) - - const rgbColor = hexToRgb(field.textColor) - dropdown.acroField.setFontSize(field.fontSize) - dropdown.acroField.setDefaultAppearance( - `0 0 0 rg /Helv ${field.fontSize} Tf` - ) - - if (field.required) dropdown.enableRequired() - if (field.readOnly) dropdown.enableReadOnly() - if (field.tooltip) { - dropdown.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - - } else if (field.type === 'optionlist') { - const optionList = form.createOptionList(field.name) - const borderRgb = hexToRgb(field.borderColor || '#000000') - optionList.addToPage(pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(1, 1, 1), - }) - if (field.options) optionList.setOptions(field.options) - if (field.defaultValue && field.options?.includes(field.defaultValue)) optionList.select(field.defaultValue) - else if (field.options && field.options.length > 0) optionList.select(field.options[0]) - - const rgbColor = hexToRgb(field.textColor) - optionList.acroField.setFontSize(field.fontSize) - optionList.acroField.setDefaultAppearance( - `0 0 0 rg /Helv ${field.fontSize} Tf` - ) - - if (field.required) optionList.enableRequired() - if (field.readOnly) optionList.enableReadOnly() - if (field.tooltip) { - optionList.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } + // Handle combing + if (field.combCells > 0) { + textField.setMaxLength(field.combCells); + textField.enableCombing(); + } else if (field.maxLength > 0) { + textField.setMaxLength(field.maxLength); + } - } else if (field.type === 'button') { - const button = form.createButton(field.name) - const borderRgb = hexToRgb(field.borderColor || '#000000') - button.addToPage(field.label || 'Button', pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: field.hideBorder ? 0 : 1, - borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), - backgroundColor: rgb(0.8, 0.8, 0.8), // Light gray - }) - - // Add Action - if (field.action && field.action !== 'none') { - const widgets = button.acroField.getWidgets() - - widgets.forEach(widget => { - let actionDict: any - - if (field.action === 'reset') { - actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'ResetForm' - }) - } else if (field.action === 'print') { - // Print action using JavaScript - actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: 'print();' - }) - } else if (field.action === 'url' && field.actionUrl) { - // Validate URL - let url = field.actionUrl.trim() - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url - } + // Disable multiline to prevent RTL issues (unless explicitly enabled) + if (!field.multiline) { + textField.disableMultiline(); + } else { + textField.enableMultiline(); + } - // Encode URL to handle special characters (RFC3986) - try { - url = encodeURI(url) - } catch (e) { - console.warn('Failed to encode URL:', e) - } + // Common properties + if (field.required) textField.enableRequired(); + if (field.readOnly) textField.enableReadOnly(); + if (field.tooltip) { + textField.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'checkbox') { + const checkBox = form.createCheckBox(field.name); + const borderRgb = hexToRgb(field.borderColor || '#000000'); + checkBox.addToPage(pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(1, 1, 1), + }); + if (field.checked) checkBox.check(); + if (field.required) checkBox.enableRequired(); + if (field.readOnly) checkBox.enableReadOnly(); + if (field.tooltip) { + checkBox.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'radio') { + const groupName = field.name; + let radioGroup; - actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'URI', - URI: PDFString.of(url) - }) - } else if (field.action === 'js' && field.jsScript) { - actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: field.jsScript - }) - } else if (field.action === 'showHide' && field.targetFieldName) { - const target = field.targetFieldName - let script = '' - - if (field.visibilityAction === 'show') { - script = `var f = this.getField("${target}"); if(f) f.display = display.visible;` - } else if (field.visibilityAction === 'hide') { - script = `var f = this.getField("${target}"); if(f) f.display = display.hidden;` - } else { - // Toggle - script = `var f = this.getField("${target}"); if(f) f.display = (f.display === display.visible) ? display.hidden : display.visible;` - } + if (radioGroups.has(groupName)) { + radioGroup = radioGroups.get(groupName); + } else { + const existingField = form.getFieldMaybe(groupName); + + if (existingField) { + radioGroup = existingField; + radioGroups.set(groupName, radioGroup); + console.log(`Using existing radio group from PDF: ${groupName}`); + } else { + radioGroup = form.createRadioGroup(groupName); + radioGroups.set(groupName, radioGroup); + console.log(`Created new radio group: ${groupName}`); + } + } - actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: script - }) - } - - if (actionDict) { - widget.dict.set(PDFName.of('A'), actionDict) - } - }) - } + const borderRgb = hexToRgb(field.borderColor || '#000000'); + radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(1, 1, 1), + }); + if (field.checked) radioGroup.select(field.exportValue || 'Yes'); + if (field.required) radioGroup.enableRequired(); + if (field.readOnly) radioGroup.enableReadOnly(); + if (field.tooltip) { + radioGroup.acroField.getWidgets().forEach((widget: any) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'dropdown') { + const dropdown = form.createDropdown(field.name); + const borderRgb = hexToRgb(field.borderColor || '#000000'); + dropdown.addToPage(pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(1, 1, 1), // Light blue not supported in standard PDF appearance easily without streams + }); + if (field.options) dropdown.setOptions(field.options); + if (field.defaultValue && field.options?.includes(field.defaultValue)) + dropdown.select(field.defaultValue); + else if (field.options && field.options.length > 0) + dropdown.select(field.options[0]); + + const rgbColor = hexToRgb(field.textColor); + dropdown.acroField.setFontSize(field.fontSize); + dropdown.acroField.setDefaultAppearance( + `0 0 0 rg /Helv ${field.fontSize} Tf` + ); + + if (field.required) dropdown.enableRequired(); + if (field.readOnly) dropdown.enableReadOnly(); + if (field.tooltip) { + dropdown.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'optionlist') { + const optionList = form.createOptionList(field.name); + const borderRgb = hexToRgb(field.borderColor || '#000000'); + optionList.addToPage(pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(1, 1, 1), + }); + if (field.options) optionList.setOptions(field.options); + if (field.defaultValue && field.options?.includes(field.defaultValue)) + optionList.select(field.defaultValue); + else if (field.options && field.options.length > 0) + optionList.select(field.options[0]); + + const rgbColor = hexToRgb(field.textColor); + optionList.acroField.setFontSize(field.fontSize); + optionList.acroField.setDefaultAppearance( + `0 0 0 rg /Helv ${field.fontSize} Tf` + ); + + if (field.required) optionList.enableRequired(); + if (field.readOnly) optionList.enableReadOnly(); + if (field.tooltip) { + optionList.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'button') { + const button = form.createButton(field.name); + const borderRgb = hexToRgb(field.borderColor || '#000000'); + button.addToPage(field.label || 'Button', pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), + backgroundColor: rgb(0.8, 0.8, 0.8), // Light gray + }); + + // Add Action + if (field.action && field.action !== 'none') { + const widgets = button.acroField.getWidgets(); + + widgets.forEach((widget) => { + let actionDict: any; + + if (field.action === 'reset') { + actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'ResetForm', + }); + } else if (field.action === 'print') { + // Print action using JavaScript + actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: 'print();', + }); + } else if (field.action === 'url' && field.actionUrl) { + // Validate URL + let url = field.actionUrl.trim(); + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + // Encode URL to handle special characters (RFC3986) + try { + url = encodeURI(url); + } catch (e) { + console.warn('Failed to encode URL:', e); + } + + actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'URI', + URI: PDFString.of(url), + }); + } else if (field.action === 'js' && field.jsScript) { + actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: field.jsScript, + }); + } else if (field.action === 'showHide' && field.targetFieldName) { + const target = field.targetFieldName; + let script = ''; + + if (field.visibilityAction === 'show') { + script = `var f = this.getField("${target}"); if(f) f.display = display.visible;`; + } else if (field.visibilityAction === 'hide') { + script = `var f = this.getField("${target}"); if(f) f.display = display.hidden;`; + } else { + // Toggle + script = `var f = this.getField("${target}"); if(f) f.display = (f.display === display.visible) ? display.hidden : display.visible;`; + } + + actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: script, + }); + } - if (field.tooltip) { - button.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - } else if (field.type === 'date') { - const dateField = form.createTextField(field.name) - dateField.addToPage(pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), - backgroundColor: rgb(1, 1, 1), - }) - - // Add Date Format and Keystroke Actions to the FIELD (not widget) - const dateFormat = field.dateFormat || 'mm/dd/yyyy' - - const formatAction = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: PDFString.of(`AFDate_FormatEx("${dateFormat}");`) - }) - - const keystrokeAction = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: PDFString.of(`AFDate_KeystrokeEx("${dateFormat}");`) - }) - - // Attach AA (Additional Actions) to the field dictionary - const additionalActions = pdfDoc.context.obj({ - F: formatAction, - K: keystrokeAction - }) - dateField.acroField.dict.set(PDFName.of('AA'), additionalActions) - - if (field.required) dateField.enableRequired() - if (field.readOnly) dateField.enableReadOnly() - if (field.tooltip) { - dateField.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - } else if (field.type === 'image') { - const imageBtn = form.createButton(field.name) - imageBtn.addToPage(field.label || 'Click to Upload Image', pdfPage, { - x: x, - y: y, - width: width, - height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), - backgroundColor: rgb(0.9, 0.9, 0.9), - }) - - // Add Import Icon Action - const widgets = imageBtn.acroField.getWidgets() - widgets.forEach(widget => { - const actionDict = pdfDoc.context.obj({ - Type: 'Action', - S: 'JavaScript', - JS: 'event.target.buttonImportIcon();' - }) - widget.dict.set(PDFName.of('A'), actionDict) - - // Set Appearance Characteristics (MK) -> Text Position (TP) = 1 (Icon Only) - // This ensures the image replaces the text when uploaded - // IF (Icon Fit) -> SW: A (Always Scale), S: A (Anamorphic/Fill) - const mkDict = pdfDoc.context.obj({ - TP: 1, - BG: [0.9, 0.9, 0.9], // Background color (Light Gray) - BC: [0, 0, 0], // Border color (Black) - IF: { - SW: PDFName.of('A'), - S: PDFName.of('A'), - FB: true - } - }) - widget.dict.set(PDFName.of('MK'), mkDict) - }) - - if (field.tooltip) { - imageBtn.acroField.getWidgets().forEach(widget => { - widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - }) - } - } else if (field.type === 'signature') { - const context = pdfDoc.context - - // Create the signature field dictionary with FT = Sig - const sigDict = context.obj({ - FT: PDFName.of('Sig'), - T: PDFString.of(field.name), - Kids: [], - }) as PDFDict - const sigRef = context.register(sigDict) - - // Create the widget annotation for the signature field - const widgetDict = context.obj({ - Type: PDFName.of('Annot'), - Subtype: PDFName.of('Widget'), - Rect: [x, y, x + width, y + height], - F: 4, // Print flag - P: pdfPage.ref, - Parent: sigRef, - }) as PDFDict - - // Add border and background appearance - const borderStyle = context.obj({ - W: 1, // Border width - S: PDFName.of('S'), // Solid border - }) as PDFDict - widgetDict.set(PDFName.of('BS'), borderStyle) - widgetDict.set(PDFName.of('BC'), context.obj([0, 0, 0])) // Border color (black) - widgetDict.set(PDFName.of('BG'), context.obj([0.95, 0.95, 0.95])) // Background color - - const widgetRef = context.register(widgetDict) - - const kidsArray = sigDict.get(PDFName.of('Kids')) as PDFArray - kidsArray.push(widgetRef) - - pdfPage.node.addAnnot(widgetRef) - - const acroForm = form.acroForm - acroForm.addField(sigRef) - - // Add tooltip if specified - if (field.tooltip) { - widgetDict.set(PDFName.of('TU'), PDFString.of(field.tooltip)) - } + if (actionDict) { + widget.dict.set(PDFName.of('A'), actionDict); } + }); } - form.updateFieldAppearances(helveticaFont) - - const pdfBytes = await pdfDoc.save() - const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }) - downloadFile(blob, 'fillable-form.pdf') - showModal('Success', 'Your PDF has been downloaded successfully.', 'info', () => { - resetToInitial() - }, 'Okay') - } catch (error) { - console.error('Error generating PDF:', error) - const errorMessage = (error as Error).message - - // Check if it's a duplicate field name error - if (errorMessage.includes('A field already exists with the specified name')) { - // Extract the field name from the error message - const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/) - const fieldName = match ? match[1] : 'unknown' - - if (existingRadioGroups.has(fieldName)) { - console.log(`Adding to existing radio group: ${fieldName}`) - } else { - showModal('Duplicate Field Name', `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, 'error') - } - } else { - showModal('Error', 'Error generating PDF: ' + errorMessage, 'error') + if (field.tooltip) { + button.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'date') { + const dateField = form.createTextField(field.name); + dateField.addToPage(pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: 1, + borderColor: rgb(0, 0, 0), + backgroundColor: rgb(1, 1, 1), + }); + + // Add Date Format and Keystroke Actions to the FIELD (not widget) + const dateFormat = field.dateFormat || 'mm/dd/yyyy'; + + const formatAction = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: PDFString.of(`AFDate_FormatEx("${dateFormat}");`), + }); + + const keystrokeAction = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: PDFString.of(`AFDate_KeystrokeEx("${dateFormat}");`), + }); + + // Attach AA (Additional Actions) to the field dictionary + const additionalActions = pdfDoc.context.obj({ + F: formatAction, + K: keystrokeAction, + }); + dateField.acroField.dict.set(PDFName.of('AA'), additionalActions); + + if (field.required) dateField.enableRequired(); + if (field.readOnly) dateField.enableReadOnly(); + if (field.tooltip) { + dateField.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); + } + } else if (field.type === 'image') { + const imageBtn = form.createButton(field.name); + imageBtn.addToPage(field.label || 'Click to Upload Image', pdfPage, { + x: x, + y: y, + width: width, + height: height, + borderWidth: 1, + borderColor: rgb(0, 0, 0), + backgroundColor: rgb(0.9, 0.9, 0.9), + }); + + // Add Import Icon Action + const widgets = imageBtn.acroField.getWidgets(); + widgets.forEach((widget) => { + const actionDict = pdfDoc.context.obj({ + Type: 'Action', + S: 'JavaScript', + JS: 'event.target.buttonImportIcon();', + }); + widget.dict.set(PDFName.of('A'), actionDict); + + // Set Appearance Characteristics (MK) -> Text Position (TP) = 1 (Icon Only) + // This ensures the image replaces the text when uploaded + // IF (Icon Fit) -> SW: A (Always Scale), S: A (Anamorphic/Fill) + const mkDict = pdfDoc.context.obj({ + TP: 1, + BG: [0.9, 0.9, 0.9], // Background color (Light Gray) + BC: [0, 0, 0], // Border color (Black) + IF: { + SW: PDFName.of('A'), + S: PDFName.of('A'), + FB: true, + }, + }); + widget.dict.set(PDFName.of('MK'), mkDict); + }); + + if (field.tooltip) { + imageBtn.acroField.getWidgets().forEach((widget) => { + widget.dict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + }); } + } else if (field.type === 'signature') { + const context = pdfDoc.context; + + // Create the signature field dictionary with FT = Sig + const sigDict = context.obj({ + FT: PDFName.of('Sig'), + T: PDFString.of(field.name), + Kids: [], + }) as PDFDict; + const sigRef = context.register(sigDict); + + // Create the widget annotation for the signature field + const widgetDict = context.obj({ + Type: PDFName.of('Annot'), + Subtype: PDFName.of('Widget'), + Rect: [x, y, x + width, y + height], + F: 4, // Print flag + P: pdfPage.ref, + Parent: sigRef, + }) as PDFDict; + + // Add border and background appearance + const borderStyle = context.obj({ + W: 1, // Border width + S: PDFName.of('S'), // Solid border + }) as PDFDict; + widgetDict.set(PDFName.of('BS'), borderStyle); + widgetDict.set(PDFName.of('BC'), context.obj([0, 0, 0])); // Border color (black) + widgetDict.set(PDFName.of('BG'), context.obj([0.95, 0.95, 0.95])); // Background color + + const widgetRef = context.register(widgetDict); + + const kidsArray = sigDict.get(PDFName.of('Kids')) as PDFArray; + kidsArray.push(widgetRef); + + pdfPage.node.addAnnot(widgetRef); + + const acroForm = form.acroForm; + acroForm.addField(sigRef); + + // Add tooltip if specified + if (field.tooltip) { + widgetDict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); + } + } } -}) + + form.updateFieldAppearances(helveticaFont); + + const pdfBytes = await pdfDoc.save(); + const blob = new Blob([new Uint8Array(pdfBytes)], { + type: 'application/pdf', + }); + downloadFile(blob, 'fillable-form.pdf'); + showModal( + 'Success', + 'Your PDF has been downloaded successfully.', + 'info', + () => { + resetToInitial(); + }, + 'Okay' + ); + } catch (error) { + console.error('Error generating PDF:', error); + const errorMessage = (error as Error).message; + + // Check if it's a duplicate field name error + if ( + errorMessage.includes('A field already exists with the specified name') + ) { + // Extract the field name from the error message + const match = errorMessage.match( + /A field already exists with the specified name: "(.+?)"/ + ); + const fieldName = match ? match[1] : 'unknown'; + + if (existingRadioGroups.has(fieldName)) { + console.log(`Adding to existing radio group: ${fieldName}`); + } else { + showModal( + 'Duplicate Field Name', + `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, + 'error' + ); + } + } else { + showModal('Error', 'Error generating PDF: ' + errorMessage, 'error'); + } + } +}); // Back to tools button -const backToToolsBtns = document.querySelectorAll('[id^="back-to-tools"]') as NodeListOf -backToToolsBtns.forEach(btn => { - btn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL - }) -}) +const backToToolsBtns = document.querySelectorAll( + '[id^="back-to-tools"]' +) as NodeListOf; +backToToolsBtns.forEach((btn) => { + btn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); +}); function getPageDimensions(size: string): { width: number; height: number } { - let dimensions: [number, number] - switch (size) { - case 'letter': - dimensions = PageSizes.Letter - break - case 'a4': - dimensions = PageSizes.A4 - break - case 'a5': - dimensions = PageSizes.A5 - break - case 'legal': - dimensions = PageSizes.Legal - break - case 'tabloid': - dimensions = PageSizes.Tabloid - break - case 'a3': - dimensions = PageSizes.A3 - break - case 'custom': - // Get custom dimensions from inputs - const width = parseInt(customWidth.value) || 612 - const height = parseInt(customHeight.value) || 792 - return { width, height } - default: - dimensions = PageSizes.Letter - } - return { width: dimensions[0], height: dimensions[1] } + let dimensions: [number, number]; + switch (size) { + case 'letter': + dimensions = PageSizes.Letter; + break; + case 'a4': + dimensions = PageSizes.A4; + break; + case 'a5': + dimensions = PageSizes.A5; + break; + case 'legal': + dimensions = PageSizes.Legal; + break; + case 'tabloid': + dimensions = PageSizes.Tabloid; + break; + case 'a3': + dimensions = PageSizes.A3; + break; + case 'custom': + // Get custom dimensions from inputs + const width = parseInt(customWidth.value) || 612; + const height = parseInt(customHeight.value) || 792; + return { width, height }; + default: + dimensions = PageSizes.Letter; + } + return { width: dimensions[0], height: dimensions[1] }; } // Reset to initial state function resetToInitial(): void { - fields = [] - pages = [] - currentPageIndex = 0 - uploadedPdfDoc = null - selectedField = null + fields = []; + pages = []; + currentPageIndex = 0; + uploadedPdfDoc = null; + selectedField = null; - canvas.innerHTML = '' + canvas.innerHTML = ''; - propertiesPanel.innerHTML = '

Select a field to edit properties

' + propertiesPanel.innerHTML = + '

Select a field to edit properties

'; - updateFieldCount() + updateFieldCount(); - // Show upload area and hide tool container - uploadArea.classList.remove('hidden') - toolContainer.classList.add('hidden') - pageSizeSelector.classList.add('hidden') - setTimeout(() => createIcons({ icons }), 100) + // Show upload area and hide tool container + uploadArea.classList.remove('hidden'); + toolContainer.classList.add('hidden'); + pageSizeSelector.classList.add('hidden'); + setTimeout(() => createIcons({ icons }), 100); } function createBlankPage(): void { - pages.push({ - index: pages.length, - width: pageSize.width, - height: pageSize.height - }) - updatePageNavigation() + pages.push({ + index: pages.length, + width: pageSize.width, + height: pageSize.height, + }); + updatePageNavigation(); } function switchToPage(pageIndex: number): void { - if (pageIndex < 0 || pageIndex >= pages.length) return + if (pageIndex < 0 || pageIndex >= pages.length) return; - currentPageIndex = pageIndex - renderCanvas() - updatePageNavigation() + currentPageIndex = pageIndex; + renderCanvas(); + updatePageNavigation(); - // Deselect any selected field when switching pages - deselectAll() + // Deselect any selected field when switching pages + deselectAll(); } // Render the canvas for the current page async function renderCanvas(): Promise { - const currentPage = pages[currentPageIndex] - if (!currentPage) return + const currentPage = pages[currentPageIndex]; + if (!currentPage) return; - // Fixed scale for better visibility - const scale = 1.333 + // Fixed scale for better visibility + const scale = 1.333; - currentScale = scale + currentScale = scale; - // Use actual PDF page dimensions (not scaled) - const canvasWidth = currentPage.width * scale - const canvasHeight = currentPage.height * scale + // Use actual PDF page dimensions (not scaled) + const canvasWidth = currentPage.width * scale; + const canvasHeight = currentPage.height * scale; - canvas.style.width = `${canvasWidth}px` - canvas.style.height = `${canvasHeight}px` + canvas.style.width = `${canvasWidth}px`; + canvas.style.height = `${canvasHeight}px`; - canvas.innerHTML = '' + canvas.innerHTML = ''; - if (uploadedPdfDoc) { + if (uploadedPdfDoc) { + try { + const arrayBuffer = await uploadedPdfDoc.save(); + const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], { + type: 'application/pdf', + }); + const blobUrl = URL.createObjectURL(blob); + + const iframe = document.createElement('iframe'); + iframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`; + iframe.style.width = '100%'; + iframe.style.height = `${canvasHeight}px`; + iframe.style.border = 'none'; + iframe.style.position = 'absolute'; + iframe.style.top = '0'; + iframe.style.left = '0'; + iframe.style.pointerEvents = 'none'; + iframe.style.opacity = '0.8'; + + iframe.onload = () => { try { - const arrayBuffer = await uploadedPdfDoc.save() - const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], { type: 'application/pdf' }) - const blobUrl = URL.createObjectURL(blob) - - const iframe = document.createElement('iframe') - iframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0` - iframe.style.width = '100%' - iframe.style.height = `${canvasHeight}px` - iframe.style.border = 'none' - iframe.style.position = 'absolute' - iframe.style.top = '0' - iframe.style.left = '0' - iframe.style.pointerEvents = 'none' - iframe.style.opacity = '0.8' - - iframe.onload = () => { - try { - const viewerWindow = iframe.contentWindow as any - if (viewerWindow && viewerWindow.PDFViewerApplication) { - const app = viewerWindow.PDFViewerApplication - - const style = viewerWindow.document.createElement('style') - style.textContent = ` + const viewerWindow = iframe.contentWindow as any; + if (viewerWindow && viewerWindow.PDFViewerApplication) { + const app = viewerWindow.PDFViewerApplication; + + const style = viewerWindow.document.createElement('style'); + style.textContent = ` * { margin: 0 !important; padding: 0 !important; @@ -2128,268 +2555,306 @@ async function renderCanvas(): Promise { border: none !important; box-shadow: none !important; } - ` - viewerWindow.document.head.appendChild(style) - - const checkRender = setInterval(() => { - if (app.pdfViewer && app.pdfViewer.pagesCount > 0) { - clearInterval(checkRender) - - const pageContainer = viewerWindow.document.querySelector('.page') - if (pageContainer) { - const initialRect = pageContainer.getBoundingClientRect() - - const offsetX = -initialRect.left - const offsetY = -initialRect.top - pageContainer.style.transform = `translate(${offsetX}px, ${offsetY}px)` - - setTimeout(() => { - const rect = pageContainer.getBoundingClientRect() - const style = viewerWindow.getComputedStyle(pageContainer) - - const borderLeft = parseFloat(style.borderLeftWidth) || 0 - const borderTop = parseFloat(style.borderTopWidth) || 0 - const borderRight = parseFloat(style.borderRightWidth) || 0 - - pdfViewerOffset = { - x: rect.left + borderLeft, - y: rect.top + borderTop - } - - const contentWidth = rect.width - borderLeft - borderRight - pdfViewerScale = contentWidth / currentPage.width - - console.log('📏 Calibrated Metrics (force positioned):', { - initialPosition: { left: initialRect.left, top: initialRect.top }, - appliedTransform: { x: offsetX, y: offsetY }, - finalRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, - computedBorders: { left: borderLeft, top: borderTop, right: borderRight }, - finalOffset: pdfViewerOffset, - finalScale: pdfViewerScale, - pdfDimensions: { width: currentPage.width, height: currentPage.height } - }) - }, 50) - } - } - }, 100) - } - } catch (e) { - console.error('Error accessing iframe content:', e) + `; + viewerWindow.document.head.appendChild(style); + + const checkRender = setInterval(() => { + if (app.pdfViewer && app.pdfViewer.pagesCount > 0) { + clearInterval(checkRender); + + const pageContainer = + viewerWindow.document.querySelector('.page'); + if (pageContainer) { + const initialRect = pageContainer.getBoundingClientRect(); + + const offsetX = -initialRect.left; + const offsetY = -initialRect.top; + pageContainer.style.transform = `translate(${offsetX}px, ${offsetY}px)`; + + setTimeout(() => { + const rect = pageContainer.getBoundingClientRect(); + const style = viewerWindow.getComputedStyle(pageContainer); + + const borderLeft = parseFloat(style.borderLeftWidth) || 0; + const borderTop = parseFloat(style.borderTopWidth) || 0; + const borderRight = parseFloat(style.borderRightWidth) || 0; + + pdfViewerOffset = { + x: rect.left + borderLeft, + y: rect.top + borderTop, + }; + + const contentWidth = rect.width - borderLeft - borderRight; + pdfViewerScale = contentWidth / currentPage.width; + + console.log('📏 Calibrated Metrics (force positioned):', { + initialPosition: { + left: initialRect.left, + top: initialRect.top, + }, + appliedTransform: { x: offsetX, y: offsetY }, + finalRect: { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }, + computedBorders: { + left: borderLeft, + top: borderTop, + right: borderRight, + }, + finalOffset: pdfViewerOffset, + finalScale: pdfViewerScale, + pdfDimensions: { + width: currentPage.width, + height: currentPage.height, + }, + }); + }, 50); } - } - - canvas.appendChild(iframe) - - console.log('Canvas dimensions:', { width: canvasWidth, height: canvasHeight, scale: currentScale }) - console.log('PDF page dimensions:', { width: currentPage.width, height: currentPage.height }) - } catch (error) { - console.error('Error rendering PDF:', error) + } + }, 100); + } + } catch (e) { + console.error('Error accessing iframe content:', e); } + }; + + canvas.appendChild(iframe); + + console.log('Canvas dimensions:', { + width: canvasWidth, + height: canvasHeight, + scale: currentScale, + }); + console.log('PDF page dimensions:', { + width: currentPage.width, + height: currentPage.height, + }); + } catch (error) { + console.error('Error rendering PDF:', error); } + } - fields.filter(f => f.pageIndex === currentPageIndex).forEach(field => { - renderField(field) - }) + fields + .filter((f) => f.pageIndex === currentPageIndex) + .forEach((field) => { + renderField(field); + }); } function updatePageNavigation(): void { - pageIndicator.textContent = `Page ${currentPageIndex + 1} of ${pages.length}` - prevPageBtn.disabled = currentPageIndex === 0 - nextPageBtn.disabled = currentPageIndex === pages.length - 1 + pageIndicator.textContent = `Page ${currentPageIndex + 1} of ${pages.length}`; + prevPageBtn.disabled = currentPageIndex === 0; + nextPageBtn.disabled = currentPageIndex === pages.length - 1; } // Drag and drop handlers for upload area dropZone.addEventListener('dragover', (e) => { - e.preventDefault() - dropZone.classList.add('border-indigo-500', 'bg-gray-600') -}) + e.preventDefault(); + dropZone.classList.add('border-indigo-500', 'bg-gray-600'); +}); dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('border-indigo-500', 'bg-gray-600') -}) + dropZone.classList.remove('border-indigo-500', 'bg-gray-600'); +}); dropZone.addEventListener('drop', (e) => { - e.preventDefault() - dropZone.classList.remove('border-indigo-500', 'bg-gray-600') - const files = e.dataTransfer?.files - if (files && files.length > 0 && files[0].type === 'application/pdf') { - handlePdfUpload(files[0]) - } -}) + e.preventDefault(); + dropZone.classList.remove('border-indigo-500', 'bg-gray-600'); + const files = e.dataTransfer?.files; + if (files && files.length > 0 && files[0].type === 'application/pdf') { + handlePdfUpload(files[0]); + } +}); pdfFileInput.addEventListener('change', async (e) => { - const file = (e.target as HTMLInputElement).files?.[0] - if (file) { - handlePdfUpload(file) - } -}) + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + handlePdfUpload(file); + } +}); blankPdfBtn.addEventListener('click', () => { - pageSizeSelector.classList.remove('hidden') -}) + pageSizeSelector.classList.remove('hidden'); +}); pageSizeSelect.addEventListener('change', () => { - if (pageSizeSelect.value === 'custom') { - customDimensionsInput.classList.remove('hidden') - } else { - customDimensionsInput.classList.add('hidden') - } -}) + if (pageSizeSelect.value === 'custom') { + customDimensionsInput.classList.remove('hidden'); + } else { + customDimensionsInput.classList.add('hidden'); + } +}); confirmBlankBtn.addEventListener('click', () => { - const selectedSize = pageSizeSelect.value - pageSize = getPageDimensions(selectedSize) + const selectedSize = pageSizeSelect.value; + pageSize = getPageDimensions(selectedSize); - createBlankPage() - switchToPage(0) + createBlankPage(); + switchToPage(0); - // Hide upload area and show tool container - uploadArea.classList.add('hidden') - toolContainer.classList.remove('hidden') - setTimeout(() => createIcons({ icons }), 100) -}) + // Hide upload area and show tool container + uploadArea.classList.add('hidden'); + toolContainer.classList.remove('hidden'); + setTimeout(() => createIcons({ icons }), 100); +}); async function handlePdfUpload(file: File) { - try { - const arrayBuffer = await file.arrayBuffer() - uploadedPdfDoc = await PDFDocument.load(arrayBuffer) + try { + const arrayBuffer = await file.arrayBuffer(); + uploadedPdfDoc = await PDFDocument.load(arrayBuffer); - // Check for existing fields and update counter - existingFieldNames.clear() - try { - const form = uploadedPdfDoc.getForm() - const pdfFields = form.getFields() + // Check for existing fields and update counter + existingFieldNames.clear(); + try { + const form = uploadedPdfDoc.getForm(); + const pdfFields = form.getFields(); - // console.log('📋 Found', pdfFields.length, 'existing fields in uploaded PDF') + // console.log('📋 Found', pdfFields.length, 'existing fields in uploaded PDF') - pdfFields.forEach(field => { - const name = field.getName() - existingFieldNames.add(name) // Track all existing field names + pdfFields.forEach((field) => { + const name = field.getName(); + existingFieldNames.add(name); // Track all existing field names - if (field instanceof PDFRadioGroup) { - existingRadioGroups.add(name) - } + if (field instanceof PDFRadioGroup) { + existingRadioGroups.add(name); + } - // console.log(' Field:', name, '| Type:', field.constructor.name) + // console.log(' Field:', name, '| Type:', field.constructor.name) - const match = name.match(/([a-zA-Z]+)_(\d+)/) - if (match) { - const num = parseInt(match[2]) - if (!isNaN(num) && num > fieldCounter) { - fieldCounter = num - console.log(' → Updated field counter to:', fieldCounter) - } - } - }) - - // TODO@ALAM: DEBUGGER - // console.log('Field counter after upload:', fieldCounter) - // console.log('Existing field names:', Array.from(existingFieldNames)) - } catch (e) { - console.log('No form fields found or error reading fields:', e) + const match = name.match(/([a-zA-Z]+)_(\d+)/); + if (match) { + const num = parseInt(match[2]); + if (!isNaN(num) && num > fieldCounter) { + fieldCounter = num; + console.log(' → Updated field counter to:', fieldCounter); + } } + }); - uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise + // TODO@ALAM: DEBUGGER + // console.log('Field counter after upload:', fieldCounter) + // console.log('Existing field names:', Array.from(existingFieldNames)) + } catch (e) { + console.log('No form fields found or error reading fields:', e); + } - const pageCount = uploadedPdfDoc.getPageCount() - pages = [] + uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise; - for (let i = 0; i < pageCount; i++) { - const page = uploadedPdfDoc.getPage(i) - const { width, height } = page.getSize() + const pageCount = uploadedPdfDoc.getPageCount(); + pages = []; - pages.push({ - index: i, - width, - height, - pdfPageData: undefined - }) - } + for (let i = 0; i < pageCount; i++) { + const page = uploadedPdfDoc.getPage(i); + const { width, height } = page.getSize(); - currentPageIndex = 0 - renderCanvas() - updatePageNavigation() + pages.push({ + index: i, + width, + height, + pdfPageData: undefined, + }); + } - // Hide upload area and show tool container - uploadArea.classList.add('hidden') - toolContainer.classList.remove('hidden') + currentPageIndex = 0; + renderCanvas(); + updatePageNavigation(); - // Init icons - setTimeout(() => createIcons({ icons }), 100) - } catch (error) { - console.error('Error loading PDF:', error) - showModal('Error', 'Error loading PDF file. Please try again with a valid PDF.', 'error') - } + // Hide upload area and show tool container + uploadArea.classList.add('hidden'); + toolContainer.classList.remove('hidden'); + + // Init icons + setTimeout(() => createIcons({ icons }), 100); + } catch (error) { + console.error('Error loading PDF:', error); + showModal( + 'Error', + 'Error loading PDF file. Please try again with a valid PDF.', + 'error' + ); + } } // Page navigation prevPageBtn.addEventListener('click', () => { - if (currentPageIndex > 0) { - switchToPage(currentPageIndex - 1) - } -}) + if (currentPageIndex > 0) { + switchToPage(currentPageIndex - 1); + } +}); nextPageBtn.addEventListener('click', () => { - if (currentPageIndex < pages.length - 1) { - switchToPage(currentPageIndex + 1) - } -}) + if (currentPageIndex < pages.length - 1) { + switchToPage(currentPageIndex + 1); + } +}); addPageBtn.addEventListener('click', () => { - createBlankPage() - switchToPage(pages.length - 1) -}) + createBlankPage(); + switchToPage(pages.length - 1); +}); resetBtn.addEventListener('click', () => { - if (fields.length > 0 || pages.length > 0) { - if (confirm('Are you sure you want to reset? All your work will be lost.')) { - resetToInitial() - } - } else { - resetToInitial() + if (fields.length > 0 || pages.length > 0) { + if ( + confirm('Are you sure you want to reset? All your work will be lost.') + ) { + resetToInitial(); } -}) + } else { + resetToInitial(); + } +}); // Custom Modal Logic -const errorModal = document.getElementById('errorModal') -const errorModalTitle = document.getElementById('errorModalTitle') -const errorModalMessage = document.getElementById('errorModalMessage') -const errorModalClose = document.getElementById('errorModalClose') - -let modalCloseCallback: (() => void) | null = null - -function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error', onClose?: () => void, buttonText: string = 'Close') { - if (!errorModal || !errorModalTitle || !errorModalMessage || !errorModalClose) return - - errorModalTitle.textContent = title - errorModalMessage.textContent = message - errorModalClose.textContent = buttonText - - modalCloseCallback = onClose || null - errorModal.classList.remove('hidden') +const errorModal = document.getElementById('errorModal'); +const errorModalTitle = document.getElementById('errorModalTitle'); +const errorModalMessage = document.getElementById('errorModalMessage'); +const errorModalClose = document.getElementById('errorModalClose'); + +let modalCloseCallback: (() => void) | null = null; + +function showModal( + title: string, + message: string, + type: 'error' | 'warning' | 'info' = 'error', + onClose?: () => void, + buttonText: string = 'Close' +) { + if (!errorModal || !errorModalTitle || !errorModalMessage || !errorModalClose) + return; + + errorModalTitle.textContent = title; + errorModalMessage.textContent = message; + errorModalClose.textContent = buttonText; + + modalCloseCallback = onClose || null; + errorModal.classList.remove('hidden'); } if (errorModalClose) { - errorModalClose.addEventListener('click', () => { - errorModal?.classList.add('hidden') - if (modalCloseCallback) { - modalCloseCallback() - modalCloseCallback = null - } - }) + errorModalClose.addEventListener('click', () => { + errorModal?.classList.add('hidden'); + if (modalCloseCallback) { + modalCloseCallback(); + modalCloseCallback = null; + } + }); } // Close modal on backdrop click if (errorModal) { - errorModal.addEventListener('click', (e) => { - if (e.target === errorModal) { - errorModal.classList.add('hidden') - if (modalCloseCallback) { - modalCloseCallback() - modalCloseCallback = null - } - } - }) + errorModal.addEventListener('click', (e) => { + if (e.target === errorModal) { + errorModal.classList.add('hidden'); + if (modalCloseCallback) { + modalCloseCallback(); + modalCloseCallback = null; + } + } + }); } -initializeGlobalShortcuts() +initializeGlobalShortcuts(); diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index 00b0a9442..762c8b25b 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -355,7 +355,9 @@
-