Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
63707f0
feat(i18n): localize status messages and UI elements across various t…
Skillkiller Apr 16, 2026
0f9eb1b
fix(i18n): update label for table of contents title in German localiz…
Skillkiller Apr 16, 2026
9cec7fa
feat(i18n): add unknown error messages to localization files and use it
Skillkiller Apr 16, 2026
7e61a54
feat(i18n): add auto-extract existing bookmarks localization for edit…
Skillkiller Apr 16, 2026
2879557
feat(i18n): add RTL language detection note to localization files and…
Skillkiller Apr 16, 2026
5934430
feat(i18n): initialize i18n before showing status message
Skillkiller Apr 16, 2026
2dd58cc
feat(i18n): initialize i18n before showing status message
Skillkiller Apr 16, 2026
cf8dd5e
fix: integrate localization for table of contents title
Skillkiller Apr 17, 2026
9bc8462
fix: add missing localization to OCR PDF tool
Skillkiller Apr 17, 2026
84f13cd
fix: add missing localization to sign PDF tool
Skillkiller Apr 17, 2026
0ea7c69
fix: add missing localization to wasm settings
Skillkiller Apr 17, 2026
b9dacf5
refactor(i18n): centralize unknownError and unify pdfToText note keys
Skillkiller Apr 17, 2026
a8b2b24
fix: update localization for addBookmark in table of contents
Skillkiller Apr 17, 2026
7f42080
fix: update localization for binarizeLabel in tools.json
Skillkiller Apr 17, 2026
0b458ce
fix: improve localization for JSON conversion messages in tools.json
Skillkiller Apr 17, 2026
4f9431b
fix: remove dupplicate localization andupdate messages in multiple la…
Skillkiller Apr 17, 2026
33d7c6b
fix: remove unused i18n initialization import in table of contents logic
Skillkiller Apr 17, 2026
a3692db
fix: initialize i18n on DOMContentLoaded
Skillkiller Apr 17, 2026
9c3f652
fix: replace hardcoded strings with localized messages for table of c…
Skillkiller Apr 17, 2026
8c97768
fix: add localization for CSV and JSON format messages in bookmark tool
Skillkiller Apr 17, 2026
f4a583a
fix: refactor event listener initialization to wait for i18n for JSON…
Skillkiller Apr 17, 2026
4e1f8c5
fix: add localization for how it works section in bookmark tool
Skillkiller Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@
"file": "Datei",
"files": "Dateien",
"close": "Schließen",
"convert": "Konvertieren"
"convert": "Konvertieren",
"unknownError": "Unbekannter Fehler"
},
"about": {
"hero": {
Expand Down
236 changes: 224 additions & 12 deletions public/locales/de/tools.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@
"file": "File",
"files": "Files",
"close": "Close",
"convert": "Convert"
"convert": "Convert",
"unknownError": "Unknown error"
},
"about": {
"hero": {
Expand Down
236 changes: 224 additions & 12 deletions public/locales/en/tools.json

Large diffs are not rendered by default.

61 changes: 34 additions & 27 deletions src/js/logic/digital-sign-pdf-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ function initializePage(): void {
const validTypes = ['image/png', 'image/jpeg', 'image/webp'];
if (!validTypes.includes(file.type)) {
showAlert(
'Invalid Image',
'Please select a PNG, JPG, or WebP image.'
t('tools:digitalSignPdf.invalidImageTitle'),
t('tools:digitalSignPdf.invalidImageMessage')
);
return;
}
Expand Down Expand Up @@ -242,7 +242,10 @@ async function handlePdfFile(file: File): Promise<void> {
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.');
showAlert(
t('tools:digitalSignPdf.invalidFileTitle'),
t('tools:digitalSignPdf.invalidFileMessage')
);
return;
}

Expand Down Expand Up @@ -307,7 +310,7 @@ async function updatePdfDisplay(): Promise<void> {
state.pdfFile = result.file;
state.pdfBytes = new Uint8Array(result.bytes);
nameSpan.textContent = result.file.name;
metaSpan.textContent = `${formatBytes(result.file.size)} • ${result.pdf.numPages} pages`;
metaSpan.textContent = `${formatBytes(result.file.size)} • ${t('tools:digitalSignPdf.pageCount', { count: result.pdf.numPages })}`;
result.pdf.destroy();
}
}
Expand Down Expand Up @@ -366,8 +369,8 @@ async function handleCertFile(file: File): Promise<void> {

if (!hasValidExtension) {
showAlert(
'Invalid Certificate',
'Please select a .pfx, .p12, or .pem certificate file.'
t('tools:digitalSignPdf.invalidCertificateTitle'),
t('tools:digitalSignPdf.invalidCertificateMessage')
);
return;
}
Expand All @@ -386,30 +389,29 @@ async function handleCertFile(file: File): Promise<void> {

if (isEncrypted) {
showPasswordSection();
updatePasswordLabel('Private Key Password');
updatePasswordLabel(t('tools:digitalSignPdf.privateKeyPassword'));
} else {
state.certData = parseCombinedPem(pemContent);
updateCertInfo();
showSignatureOptions();

const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) {
certStatus.innerHTML =
'Certificate loaded <i data-lucide="check" class="inline w-4 h-4"></i>';
certStatus.innerHTML = `${t('tools:digitalSignPdf.certificateLoaded')} <i data-lucide="check" class="inline w-4 h-4"></i>`;
createIcons({ icons });
certStatus.className = 'text-xs text-green-400';
}
}
} catch {
const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) {
certStatus.textContent = 'Failed to parse PEM file';
certStatus.textContent = t('tools:digitalSignPdf.failedParsePem');
certStatus.className = 'text-xs text-red-400';
}
}
} else {
showPasswordSection();
updatePasswordLabel('Certificate Password');
updatePasswordLabel(t('tools:digitalSignPdf.certPassword'));
}

hideSignatureOptions();
Expand Down Expand Up @@ -437,7 +439,7 @@ function updateCertDisplay(): void {
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.id = 'cert-status';
metaSpan.textContent = 'Enter password to unlock';
metaSpan.textContent = t('tools:digitalSignPdf.enterPasswordToUnlock');

infoContainer.append(nameSpan, metaSpan);

Expand Down Expand Up @@ -550,8 +552,7 @@ async function handlePasswordInput(): Promise<void> {

const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) {
certStatus.innerHTML =
'Certificate unlocked <i data-lucide="check-circle" class="inline w-4 h-4"></i>';
certStatus.innerHTML = `${t('tools:digitalSignPdf.certificateUnlocked')} <i data-lucide="check-circle" class="inline w-4 h-4"></i>`;
createIcons({ icons });
certStatus.className = 'text-xs text-green-400';
}
Expand All @@ -565,10 +566,10 @@ async function handlePasswordInput(): Promise<void> {
const errorMessage =
error instanceof Error
? error.message
: 'Invalid password or certificate';
: t('tools:digitalSignPdf.invalidPasswordOrCertificate');
certStatus.textContent = errorMessage.includes('password')
? 'Incorrect password'
: 'Failed to parse certificate';
? t('tools:digitalSignPdf.incorrectPassword')
: t('tools:digitalSignPdf.failedParseCertificate');
Comment thread
Skillkiller marked this conversation as resolved.
certStatus.className = 'text-xs text-red-400';
}
}
Expand Down Expand Up @@ -616,8 +617,8 @@ function updateProcessButton(): void {
async function processSignature(): Promise<void> {
if (!state.pdfBytes || !state.certData) {
showAlert(
'Missing Data',
'Please upload both a PDF and a valid certificate.'
t('tools:digitalSignPdf.missingDataTitle'),
t('tools:digitalSignPdf.missingDataMessage')
);
return;
}
Expand Down Expand Up @@ -699,7 +700,10 @@ async function processSignature(): Promise<void> {
if (!state.sigImageData && !sigText && state.certData) {
const certInfo = getCertificateInfo(state.certData.certificate);
const date = new Date().toLocaleDateString();
sigText = `Digitally signed by ${certInfo.subject}\n${date}`;
sigText = t('tools:digitalSignPdf.defaultSignatureText', {
signer: certInfo.subject,
date,
});
}

let finalHeight = sigHeight;
Expand Down Expand Up @@ -728,7 +732,7 @@ async function processSignature(): Promise<void> {
};
}

showLoader('Applying digital signature...');
showLoader(t('tools:digitalSignPdf.applyingSignature'));

try {
const signedPdfBytes = await signPdf(state.pdfBytes, state.certData, {
Expand All @@ -743,8 +747,8 @@ async function processSignature(): Promise<void> {

hideLoader();
showAlert(
'Success',
'PDF signed successfully! The signature can be verified in any PDF reader.',
t('common.success'),
t('tools:digitalSignPdf.successMessage'),
'success',
() => {
resetState();
Expand All @@ -754,7 +758,7 @@ async function processSignature(): Promise<void> {
hideLoader();
console.error('Signing error:', error);
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
error instanceof Error ? error.message : t('common.unknownError');

// Check if this is a CORS/network error from certificate chain fetching
if (
Expand All @@ -763,11 +767,14 @@ async function processSignature(): Promise<void> {
errorMessage.includes('NetworkError')
) {
showAlert(
'Signing Failed',
'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.'
t('tools:digitalSignPdf.signingFailedTitle'),
t('tools:digitalSignPdf.chainFetchFailed')
);
} else {
showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`);
showAlert(
t('tools:digitalSignPdf.signingFailedTitle'),
t('tools:digitalSignPdf.failedToSignPdf', { message: errorMessage })
);
}
}
}
Expand Down
46 changes: 29 additions & 17 deletions src/js/logic/json-to-pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
showWasmRequiredDialog,
WasmProvider,
} from '../utils/wasm-provider.js';
import { initI18n, t } from '../i18n/i18n';

const worker = new Worker(
import.meta.env.BASE_URL + 'workers/json-to-pdf.worker.js'
Expand Down Expand Up @@ -80,10 +81,12 @@ jsonFilesInput.addEventListener('change', (e) => {
updateFileList();

if (selectedFiles.length === 0) {
showStatus('Please select at least 1 JSON file', 'info');
showStatus(t('tools:jsonToPdf.status.selectAtLeastOne'), 'info');
} else {
showStatus(
`${selectedFiles.length} file(s) selected. Ready to convert!`,
t('tools:jsonToPdf.status.selectedReady', {
count: selectedFiles.length,
}),
'info'
);
}
Expand All @@ -92,7 +95,7 @@ jsonFilesInput.addEventListener('change', (e) => {

async function convertJSONsToPDF() {
if (selectedFiles.length === 0) {
showStatus('Please select at least 1 JSON file', 'error');
showStatus(t('tools:jsonToPdf.status.selectAtLeastOne'), 'error');
return;
}

Expand All @@ -104,13 +107,13 @@ async function convertJSONsToPDF() {

try {
convertBtn.disabled = true;
showStatus('Reading files (Main Thread)...', 'info');
showStatus(t('tools:jsonToPdf.status.readingFiles'), 'info');

const fileBuffers = await Promise.all(
selectedFiles.map((file) => readFileAsArrayBuffer(file))
);

showStatus('Converting JSONs to PDFs...', 'info');
showStatus(t('tools:jsonToPdf.status.converting'), 'info');

worker.postMessage(
{
Expand All @@ -124,7 +127,10 @@ async function convertJSONsToPDF() {
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`,
t('tools:jsonToPdf.status.readError', {
message:
error instanceof Error ? error.message : t('common.unknownError'),
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'error'
);
convertBtn.disabled = false;
Expand All @@ -141,7 +147,7 @@ worker.onmessage = async (e: MessageEvent) => {
}>;

try {
showStatus('Creating ZIP file...', 'info');
showStatus(t('tools:jsonToPdf.status.creatingZip'), 'info');

const zip = new JSZip();
pdfFiles.forEach(({ name, data }) => {
Expand All @@ -157,10 +163,7 @@ worker.onmessage = async (e: MessageEvent) => {
a.download = 'jsons-to-pdf.zip';
downloadFile(zipBlob, 'jsons-to-pdf.zip');

showStatus(
'✅ JSONs converted to PDF successfully! ZIP download started.',
'success'
);
showStatus(t('tools:jsonToPdf.status.success'), 'success');

selectedFiles = [];
jsonFilesInput.value = '';
Expand All @@ -174,14 +177,20 @@ worker.onmessage = async (e: MessageEvent) => {
} catch (error) {
console.error('Error creating ZIP:', error);
showStatus(
`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`,
t('tools:jsonToPdf.status.zipError', {
message:
error instanceof Error ? error.message : t('common.unknownError'),
}),
'error'
);
}
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
const errorMessage = e.data.message || t('common.unknownError');
console.error('Worker Error:', errorMessage);
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
showStatus(
t('tools:jsonToPdf.status.workerError', { message: errorMessage }),
'error'
);
}
};

Expand All @@ -193,6 +202,9 @@ if (backToToolsBtn) {

convertBtn.addEventListener('click', convertJSONsToPDF);

// Initialize
showStatus('Select JSON files to get started', 'info');
initializeGlobalShortcuts();
// Initialize after i18n is ready so the default status is translated.
void (async () => {
await initI18n();
showStatus(t('tools:jsonToPdf.status.getStarted'), 'info');
initializeGlobalShortcuts();
})();
Loading
Loading