setup i18n and ported all tools to standalone pages

This commit is contained in:
abdullahalam123
2025-12-11 19:34:14 +05:30
parent fe3e54f979
commit 78dc6333f9
221 changed files with 30351 additions and 11131 deletions

View File

@@ -1,6 +1,5 @@
import { categories } from './config/tools.js';
import { dom, switchView, hideAlert, showLoader, hideLoader, showAlert } from './ui.js';
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { state, resetState } from './state.js';
import { ShortcutsManager } from './logic/shortcuts.js';
import { createIcons, icons } from 'lucide';
@@ -8,12 +7,14 @@ import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css';
import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
import { APP_VERSION, injectVersion } from '../version.js';
import { initI18n, applyTranslations, rewriteLinks, injectLanguageSwitcher, t } from './i18n/index.js';
const init = async () => {
await initI18n();
injectLanguageSwitcher();
applyTranslations();
const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
// Handle simple mode - hide branding sections but keep logo and copyright
// Handle simple mode - hide branding sections but keep logo and copyright
if (__SIMPLE_MODE__) {
const hideBrandingSections = () => {
// Hide navigation but keep logo
@@ -147,134 +148,224 @@ const init = () => {
const shortcutSettingsBtn = document.getElementById('open-shortcuts-btn');
if (isMobile) {
keyboardShortcutBtn.style.display = 'none';
shortcutSettingsBtn.style.display = 'none';
if (keyboardShortcutBtn) keyboardShortcutBtn.style.display = 'none';
if (shortcutSettingsBtn) shortcutSettingsBtn.style.display = 'none';
} else {
keyboardShortcutBtn.textContent = navigator.userAgent.toUpperCase().includes('MAC')
? '⌘ + K'
: 'Ctrl + K';
if (keyboardShortcutBtn) {
keyboardShortcutBtn.textContent = navigator.userAgent.toUpperCase().includes('MAC')
? '⌘ + K'
: 'Ctrl + K';
}
}
dom.toolGrid.textContent = '';
categories.forEach((category) => {
const categoryGroup = document.createElement('div');
categoryGroup.className = 'category-group col-span-full';
const title = document.createElement('h2');
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0 text-white';
title.textContent = category.name;
const toolsContainer = document.createElement('div');
toolsContainer.className =
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
category.tools.forEach((tool) => {
let toolCard: HTMLDivElement | HTMLAnchorElement;
if (tool.href) {
toolCard = document.createElement('a');
toolCard.href = tool.href;
toolCard.className =
'tool-card block bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center no-underline hover:shadow-lg transition duration-200';
} else {
toolCard = document.createElement('div');
toolCard.className =
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center hover:shadow-lg transition duration-200';
toolCard.dataset.toolId = tool.id;
}
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
toolName.textContent = tool.name;
toolCard.append(icon, toolName);
if (tool.subtitle) {
const toolSubtitle = document.createElement('p');
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
toolSubtitle.textContent = tool.subtitle;
toolCard.appendChild(toolSubtitle);
}
toolsContainer.appendChild(toolCard);
});
categoryGroup.append(title, toolsContainer);
dom.toolGrid.appendChild(categoryGroup);
});
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
const fuzzyMatch = (searchTerm: string, targetText: string): boolean => {
if (!searchTerm) return true;
let searchIndex = 0;
let targetIndex = 0;
while (searchIndex < searchTerm.length && targetIndex < targetText.length) {
if (searchTerm[searchIndex] === targetText[targetIndex]) {
searchIndex++;
}
targetIndex++;
}
return searchIndex === searchTerm.length;
const categoryTranslationKeys: Record<string, string> = {
'Popular Tools': 'tools:categories.popularTools',
'Edit & Annotate': 'tools:categories.editAnnotate',
'Convert to PDF': 'tools:categories.convertToPdf',
'Convert from PDF': 'tools:categories.convertFromPdf',
'Organize & Manage': 'tools:categories.organizeManage',
'Optimize & Repair': 'tools:categories.optimizeRepair',
'Secure PDF': 'tools:categories.securePdf',
};
searchBar.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = searchBar.value.toLowerCase().trim();
const toolTranslationKeys: Record<string, string> = {
'PDF Multi Tool': 'tools:pdfMultiTool',
'Merge PDF': 'tools:mergePdf',
'Split PDF': 'tools:splitPdf',
'Compress PDF': 'tools:compressPdf',
'PDF Editor': 'tools:pdfEditor',
'JPG to PDF': 'tools:jpgToPdf',
'Sign PDF': 'tools:signPdf',
'Crop PDF': 'tools:cropPdf',
'Extract Pages': 'tools:extractPages',
'Duplicate & Organize': 'tools:duplicateOrganize',
'Delete Pages': 'tools:deletePages',
'Edit Bookmarks': 'tools:editBookmarks',
'Table of Contents': 'tools:tableOfContents',
'Page Numbers': 'tools:pageNumbers',
'Add Watermark': 'tools:addWatermark',
'Header & Footer': 'tools:headerFooter',
'Invert Colors': 'tools:invertColors',
'Background Color': 'tools:backgroundColor',
'Change Text Color': 'tools:changeTextColor',
'Add Stamps': 'tools:addStamps',
'Remove Annotations': 'tools:removeAnnotations',
'PDF Form Filler': 'tools:pdfFormFiller',
'Create PDF Form': 'tools:createPdfForm',
'Remove Blank Pages': 'tools:removeBlankPages',
'Image to PDF': 'tools:imageToPdf',
'PNG to PDF': 'tools:pngToPdf',
'WebP to PDF': 'tools:webpToPdf',
'SVG to PDF': 'tools:svgToPdf',
'BMP to PDF': 'tools:bmpToPdf',
'HEIC to PDF': 'tools:heicToPdf',
'TIFF to PDF': 'tools:tiffToPdf',
'Text to PDF': 'tools:textToPdf',
'JSON to PDF': 'tools:jsonToPdf',
'PDF to JPG': 'tools:pdfToJpg',
'PDF to PNG': 'tools:pdfToPng',
'PDF to WebP': 'tools:pdfToWebp',
'PDF to BMP': 'tools:pdfToBmp',
'PDF to TIFF': 'tools:pdfToTiff',
'PDF to Greyscale': 'tools:pdfToGreyscale',
'PDF to JSON': 'tools:pdfToJson',
'OCR PDF': 'tools:ocrPdf',
'Alternate & Mix Pages': 'tools:alternateMix',
'Organize & Duplicate': 'tools:duplicateOrganize',
'Add Attachments': 'tools:addAttachments',
'Extract Attachments': 'tools:extractAttachments',
'Edit Attachments': 'tools:editAttachments',
'Divide Pages': 'tools:dividePages',
'Add Blank Page': 'tools:addBlankPage',
'Reverse Pages': 'tools:reversePages',
'Rotate PDF': 'tools:rotatePdf',
'N-Up PDF': 'tools:nUpPdf',
'Combine to Single Page': 'tools:combineToSinglePage',
'View Metadata': 'tools:viewMetadata',
'Edit Metadata': 'tools:editMetadata',
'PDFs to ZIP': 'tools:pdfsToZip',
'Compare PDFs': 'tools:comparePdfs',
'Posterize PDF': 'tools:posterizePdf',
'Fix Page Size': 'tools:fixPageSize',
'Linearize PDF': 'tools:linearizePdf',
'Page Dimensions': 'tools:pageDimensions',
'Remove Restrictions': 'tools:removeRestrictions',
'Repair PDF': 'tools:repairPdf',
'Encrypt PDF': 'tools:encryptPdf',
'Sanitize PDF': 'tools:sanitizePdf',
'Decrypt PDF': 'tools:decryptPdf',
'Flatten PDF': 'tools:flattenPdf',
'Remove Metadata': 'tools:removeMetadata',
'Change Permissions': 'tools:changePermissions',
};
categoryGroups.forEach((group) => {
const toolCards = group.querySelectorAll('.tool-card');
let visibleToolsInCategory = 0;
// Homepage-only tool grid rendering (not used on individual tool pages)
if (dom.toolGrid) {
dom.toolGrid.textContent = '';
toolCards.forEach((card) => {
const toolName = card.querySelector('h3').textContent.toLowerCase();
const toolSubtitle =
card.querySelector('p')?.textContent.toLowerCase() || '';
categories.forEach((category) => {
const categoryGroup = document.createElement('div');
categoryGroup.className = 'category-group col-span-full';
const isMatch =
fuzzyMatch(searchTerm, toolName) || fuzzyMatch(searchTerm, toolSubtitle);
const title = document.createElement('h2');
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0 text-white';
const categoryKey = categoryTranslationKeys[category.name];
title.textContent = categoryKey ? t(categoryKey) : category.name;
card.classList.toggle('hidden', !isMatch);
if (isMatch) {
visibleToolsInCategory++;
const toolsContainer = document.createElement('div');
toolsContainer.className =
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
category.tools.forEach((tool) => {
let toolCard: HTMLDivElement | HTMLAnchorElement;
if (tool.href) {
toolCard = document.createElement('a');
toolCard.href = tool.href;
toolCard.className =
'tool-card block bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center no-underline hover:shadow-lg transition duration-200';
} else {
toolCard = document.createElement('div');
toolCard.className =
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center hover:shadow-lg transition duration-200';
toolCard.dataset.toolId = getToolId(tool);
}
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
const toolKey = toolTranslationKeys[tool.name];
toolName.textContent = toolKey ? t(`${toolKey}.name`) : tool.name;
toolCard.append(icon, toolName);
if (tool.subtitle) {
const toolSubtitle = document.createElement('p');
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
toolSubtitle.textContent = toolKey ? t(`${toolKey}.subtitle`) : tool.subtitle;
toolCard.appendChild(toolSubtitle);
}
toolsContainer.appendChild(toolCard);
});
group.classList.toggle('hidden', visibleToolsInCategory === 0);
categoryGroup.append(title, toolsContainer);
dom.toolGrid.appendChild(categoryGroup);
});
});
window.addEventListener('keydown', function (e) {
const key = e.key.toLowerCase();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const isCtrlK = e.ctrlKey && key === 'k';
const isCmdK = isMac && e.metaKey && key === 'k';
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
if (isCtrlK || isCmdK) {
e.preventDefault();
searchBar.focus();
}
});
const fuzzyMatch = (searchTerm: string, targetText: string): boolean => {
if (!searchTerm) return true;
dom.toolGrid.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const card = e.target.closest('.tool-card');
if (card) {
const toolId = card.dataset.toolId;
setupToolInterface(toolId);
}
});
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
dom.alertOkBtn.addEventListener('click', hideAlert);
let searchIndex = 0;
let targetIndex = 0;
while (searchIndex < searchTerm.length && targetIndex < targetText.length) {
if (searchTerm[searchIndex] === targetText[targetIndex]) {
searchIndex++;
}
targetIndex++;
}
return searchIndex === searchTerm.length;
};
searchBar.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = searchBar.value.toLowerCase().trim();
categoryGroups.forEach((group) => {
const toolCards = group.querySelectorAll('.tool-card');
let visibleToolsInCategory = 0;
toolCards.forEach((card) => {
const toolName = card.querySelector('h3').textContent.toLowerCase();
const toolSubtitle =
card.querySelector('p')?.textContent.toLowerCase() || '';
const isMatch =
fuzzyMatch(searchTerm, toolName) || fuzzyMatch(searchTerm, toolSubtitle);
card.classList.toggle('hidden', !isMatch);
if (isMatch) {
visibleToolsInCategory++;
}
});
group.classList.toggle('hidden', visibleToolsInCategory === 0);
});
});
window.addEventListener('keydown', function (e) {
const key = e.key.toLowerCase();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const isCtrlK = e.ctrlKey && key === 'k';
const isCmdK = isMac && e.metaKey && key === 'k';
if (isCtrlK || isCmdK) {
e.preventDefault();
searchBar.focus();
}
});
dom.toolGrid.addEventListener('click', (e) => {
// All tools now use href and navigate directly - no modal handling needed
});
}
if (dom.backToGridBtn) {
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
}
if (dom.alertOkBtn) {
dom.alertOkBtn.addEventListener('click', hideAlert);
}
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
@@ -296,14 +387,6 @@ const init = () => {
});
}
if (window.location.hash.startsWith('#tool-')) {
const toolId = window.location.hash.substring(6);
setTimeout(() => {
setupToolInterface(toolId);
history.replaceState(null, '', window.location.pathname);
}, 100);
}
createIcons({ icons });
console.log('Please share our tool and share the love!');
@@ -438,8 +521,8 @@ const init = () => {
if (dom.resetShortcutsBtn) {
dom.resetShortcutsBtn.addEventListener('click', async () => {
const confirmed = await showWarningModal(
'Reset Shortcuts',
'Are you sure you want to reset all shortcuts to default?<br><br>This action cannot be undone.',
t('settings.warnings.resetTitle'),
t('settings.warnings.resetMessage'),
true
);
@@ -470,14 +553,14 @@ const init = () => {
if (ShortcutsManager.importSettings(content)) {
renderShortcutsList();
await showWarningModal(
'Import Successful',
'Shortcuts imported successfully!',
t('settings.warnings.importSuccessTitle'),
t('settings.warnings.importSuccessMessage'),
false
);
} else {
await showWarningModal(
'Import Failed',
'Failed to import shortcuts. Invalid file format.',
t('settings.warnings.importFailTitle'),
t('settings.warnings.importFailMessage'),
false
);
}
@@ -623,7 +706,9 @@ const init = () => {
const header = document.createElement('h3');
header.className = 'text-gray-400 text-xs font-bold uppercase tracking-wider mb-3 pl-1';
header.textContent = category.name;
// Translate category name
const categoryKey = categoryTranslationKeys[category.name];
header.textContent = categoryKey ? t(categoryKey) : category.name;
section.appendChild(header);
const itemsContainer = document.createElement('div');
@@ -649,7 +734,8 @@ const init = () => {
const name = document.createElement('span');
name.className = 'text-gray-200 font-medium';
name.textContent = tool.name;
const toolKey = toolTranslationKeys[tool.name];
name.textContent = toolKey ? t(`${toolKey}.name`) : tool.name;
left.append(icon, name);
@@ -659,7 +745,7 @@ const init = () => {
const input = document.createElement('input');
input.type = 'text';
input.className = 'shortcut-input w-32 bg-gray-800 border border-gray-600 text-white text-center text-sm rounded px-2 py-1 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all';
input.placeholder = 'Click to set';
input.placeholder = t('settings.clickToSet');
input.value = formatShortcutDisplay(currentShortcut, isMac);
input.readOnly = true;
@@ -733,11 +819,14 @@ const init = () => {
const existingToolName = existingTool?.name || existingToolId;
const displayCombo = formatShortcutDisplay(combo, isMac);
const existingToolKey = existingTool ? toolTranslationKeys[existingTool.name] : null;
const translatedToolName = existingToolKey ? t(`${existingToolKey}.name`) : existingToolName;
await showWarningModal(
'Shortcut Already in Use',
`<strong>${displayCombo}</strong> is already assigned to:<br><br>` +
`<em>"${existingToolName}"</em><br><br>` +
`Please choose a different shortcut.`,
t('settings.warnings.alreadyInUse'),
`<strong>${displayCombo}</strong> ${t('settings.warnings.assignedTo')}<br><br>` +
`<em>"${translatedToolName}"</em><br><br>` +
t('settings.warnings.chooseDifferent'),
false
);
@@ -747,16 +836,15 @@ const init = () => {
return;
}
// Check if this is a reserved shortcut
const reservedWarning = getReservedShortcutWarning(combo, isMac);
if (reservedWarning) {
const displayCombo = formatShortcutDisplay(combo, isMac);
const shouldProceed = await showWarningModal(
'Reserved Shortcut Warning',
`<strong>${displayCombo}</strong> is commonly used for:<br><br>` +
t('settings.warnings.reserved'),
`<strong>${displayCombo}</strong> ${t('settings.warnings.commonlyUsed')}<br><br>` +
`"<em>${reservedWarning}</em>"<br><br>` +
`This shortcut may not work reliably or might conflict with browser/system behavior.<br><br>` +
`Do you want to use it anyway?`
`${t('settings.warnings.unreliable')}<br><br>` +
t('settings.warnings.useAnyway')
);
if (!shouldProceed) {
@@ -783,7 +871,7 @@ const init = () => {
};
input.onfocus = () => {
input.value = 'Press keys...';
input.value = t('settings.pressKeys');
input.classList.add('border-indigo-500', 'text-indigo-400');
};
@@ -831,6 +919,9 @@ const init = () => {
});
});
}
// Rewrite links after all dynamic content is fully loaded
rewriteLinks();
};
document.addEventListener('DOMContentLoaded', init);
window.addEventListener('load', init);