fix: resolve i18n issues - URL duplication, translation loading, and caching

- Fix URL path duplication when clicking logo (added missing leading slash)
- Use network-first caching for translation files in service worker
- Add missing translation keys (common.close, upload.clearFiles) to all languages
- Add Dutch (nl) language support to URL regex patterns
- Bump service worker cache version to v8
This commit is contained in:
alam00000
2026-01-26 22:29:51 +05:30
parent 2d99a28b07
commit 62c373d76a
16 changed files with 10397 additions and 7524 deletions

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs oder Bilder",
"filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.",
"addMore": "Weitere Dateien hinzufügen",
"clearAll": "Alle löschen"
"clearAll": "Alle löschen",
"clearFiles": "Dateien löschen"
},
"loader": {
"processing": "Verarbeitung..."
@@ -226,7 +227,8 @@
"error": "Fehler",
"success": "Erfolg",
"file": "Datei",
"files": "Dateien"
"files": "Dateien",
"close": "Schließen"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs or Images",
"filesNeverLeave": "Your files never leave your device.",
"addMore": "Add More Files",
"clearAll": "Clear All"
"clearAll": "Clear All",
"clearFiles": "Clear Files"
},
"loader": {
"processing": "Processing..."
@@ -226,7 +227,8 @@
"error": "Error",
"success": "Success",
"file": "File",
"files": "Files"
"files": "Files",
"close": "Close"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs o Imágenes",
"filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.",
"addMore": "Agregar Más Archivos",
"clearAll": "Limpiar Todo"
"clearAll": "Limpiar Todo",
"clearFiles": "Borrar archivos"
},
"loader": {
"processing": "Procesando..."
@@ -226,7 +227,8 @@
"error": "Error",
"success": "Éxito",
"file": "Archivo",
"files": "Archivos"
"files": "Archivos",
"close": "Cerrar"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF ou images",
"filesNeverLeave": "Vos fichiers restent sur votre appareil.",
"addMore": "Ajouter dautres fichiers",
"clearAll": "Tout effacer"
"clearAll": "Tout effacer",
"clearFiles": "Effacer les fichiers"
},
"loader": {
"processing": "Traitement en cours..."
@@ -226,7 +227,8 @@
"error": "Erreur",
"success": "Succès",
"file": "Fichier",
"files": "Fichiers"
"files": "Fichiers",
"close": "Fermer"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF atau Gambar",
"filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.",
"addMore": "Tambah Lebih Banyak File",
"clearAll": "Hapus Semua"
"clearAll": "Hapus Semua",
"clearFiles": "Hapus file"
},
"loader": {
"processing": "Memproses..."
@@ -226,7 +227,8 @@
"error": "Kesalahan",
"success": "Berhasil",
"file": "File",
"files": "File"
"files": "File",
"close": "Tutup"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF o immagini",
"filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.",
"addMore": "Aggiungi altri file",
"clearAll": "Svuota tutto"
"clearAll": "Svuota tutto",
"clearFiles": "Cancella file"
},
"loader": {
"processing": "Elaborazione..."
@@ -226,7 +227,8 @@
"error": "Errore",
"success": "Successo",
"file": "File",
"files": "File"
"files": "File",
"close": "Chiudi"
},
"about": {
"hero": {

View File

@@ -57,7 +57,7 @@
"toolsLabel": "Tools",
"subtitle": "Klik een tool om de bestandslader te openen",
"searchPlaceholder": "Zoek een tool (bijv., 'splitsen', 'organiseren'...)",
"backToTools": "Terug naar Tools"
"backToTools": "Terug naar Tools",
"firstLoadNotice": "De eerste keer duurt het even om onze conversiemachine te laden. Daarna gaat alles meteen."
},
"upload": {
@@ -66,7 +66,8 @@
"pdfOrImages": "PDF's of Afbeeldingen",
"filesNeverLeave": "Je bestanden blijven op jouw apparaat.",
"addMore": "Meer bestanden toevoegen",
"clearAll": "Alles wissen"
"clearAll": "Alles wissen",
"clearFiles": "Bestanden wissen"
},
"loader": {
"processing": "Verwerken..."
@@ -226,7 +227,8 @@
"error": "Fout",
"success": "Success",
"file": "Bestand",
"files": "Bestanden"
"files": "Bestanden",
"close": "Sluiten"
},
"about": {
"hero": {
@@ -321,4 +323,3 @@
"failedToLoad": "Laden is mislukt"
}
}

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs ou Imagens",
"filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.",
"addMore": "Adicionar Mais Arquivos",
"clearAll": "Limpar Tudo"
"clearAll": "Limpar Tudo",
"clearFiles": "Limpar arquivos"
},
"loader": {
"processing": "Processando..."
@@ -226,7 +227,8 @@
"error": "Erro",
"success": "Sucesso",
"file": "Arquivo",
"files": "Arquivos"
"files": "Arquivos",
"close": "Fechar"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"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"
"clearAll": "Tümünü Temizle",
"clearFiles": "Dosyaları temizle"
},
"loader": {
"processing": "İşleniyor..."
@@ -226,7 +227,8 @@
"error": "Hata",
"success": "Başarılı",
"file": "Dosya",
"files": "Dosya"
"files": "Dosya",
"close": "Kapat"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF hoặc Hình ảnh",
"filesNeverLeave": "Tệp của bạn không bao giờ rời khỏi thiết bị.",
"addMore": "Thêm tệp",
"clearAll": "Xóa tất cả"
"clearAll": "Xóa tất cả",
"clearFiles": "Xóa tệp"
},
"loader": {
"processing": "Đang xử lý..."
@@ -226,7 +227,8 @@
"error": "Lỗi",
"success": "Thành công",
"file": "Tệp",
"files": "Tệp"
"files": "Tệp",
"close": "Đóng"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF 或圖片",
"filesNeverLeave": "你的檔案永遠不會離開你的裝置。",
"addMore": "添加更多檔案",
"clearAll": "清除全部"
"clearAll": "清除全部",
"clearFiles": "清除檔案"
},
"loader": {
"processing": "正在處理..."
@@ -226,7 +227,8 @@
"error": "錯誤",
"success": "成功",
"file": "檔案",
"files": "檔案"
"files": "檔案",
"close": "關閉"
},
"about": {
"hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF 或图片",
"filesNeverLeave": "您的文件从未离开您的设备。",
"addMore": "添加更多文件",
"clearAll": "清空所有"
"clearAll": "清空所有",
"clearFiles": "清除文件"
},
"loader": {
"processing": "处理中..."
@@ -226,7 +227,8 @@
"error": "错误",
"success": "成功",
"file": "文件",
"files": "文件"
"files": "文件",
"close": "关闭"
},
"about": {
"hero": {

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,9 @@
* Version: 1.1.0
*/
const CACHE_VERSION = 'bentopdf-v7';
const CACHE_VERSION = 'bentopdf-v8';
const CACHE_NAME = `${CACHE_VERSION}-static`;
const getBasePath = () => {
const scope = self.registration?.scope || self.location.href;
const url = new URL(scope);
@@ -44,7 +43,8 @@ self.addEventListener('install', (event) => {
// console.log('📦 [ServiceWorker] Will cache', CRITICAL_ASSETS.length, 'critical assets');
event.waitUntil(
caches.open(CACHE_NAME)
caches
.open(CACHE_NAME)
.then((cache) => {
// console.log('[ServiceWorker] Caching critical assets...');
return cacheInBatches(cache, CRITICAL_ASSETS, 5);
@@ -64,7 +64,8 @@ self.addEventListener('activate', (event) => {
// console.log('🔄 [ServiceWorker] Activating version:', CACHE_VERSION);
event.waitUntil(
caches.keys()
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
@@ -92,18 +93,33 @@ self.addEventListener('fetch', (event) => {
if (!isLocal && !isCDN) {
return;
}
if (isLocal && (url.searchParams.has('t') || url.searchParams.has('import') || url.searchParams.has('direct'))) {
if (
isLocal &&
(url.searchParams.has('t') ||
url.searchParams.has('import') ||
url.searchParams.has('direct'))
) {
// console.log('🔧 [Dev Mode] Skipping Vite HMR request:', url.pathname);
return;
}
if (isLocal && (url.pathname.includes('/@vite') || url.pathname.includes('/@id') || url.pathname.includes('/@fs'))) {
if (
isLocal &&
(url.pathname.includes('/@vite') ||
url.pathname.includes('/@id') ||
url.pathname.includes('/@fs'))
) {
return;
}
if (shouldCache(url.pathname, isCDN)) {
if (isLocal && url.pathname.includes('/locales/')) {
event.respondWith(networkFirstStrategy(event.request));
} else if (shouldCache(url.pathname, isCDN)) {
event.respondWith(cacheFirstStrategyWithDedup(event.request, isCDN));
} else if (isLocal && (url.pathname.endsWith('.html') || url.pathname === '/')) {
} else if (
isLocal &&
(url.pathname.endsWith('.html') || url.pathname === '/')
) {
event.respondWith(networkFirstStrategy(event.request));
}
});
@@ -154,7 +170,10 @@ async function cacheFirstStrategyWithDedup(request, isCDN) {
return fallbackResponse;
}
} catch (fallbackError) {
console.error('[ServiceWorker] Both CDN and local failed for:', fileName);
console.error(
'[ServiceWorker] Both CDN and local failed for:',
fileName
);
}
}
}
@@ -252,7 +271,9 @@ function shouldCache(pathname, isCDN = false) {
pathname.includes('/ghostscript-wasm/') ||
pathname.includes('/embedpdf/') ||
pathname.includes('/assets/') ||
pathname.match(/\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/)
pathname.match(
/\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/
)
);
}
@@ -269,7 +290,10 @@ async function cacheInBatches(cache, urls, batchSize = 5) {
try {
await cache.add(url);
const fileName = url.split('/').pop();
const fileSize = fileName.includes('.wasm') || fileName.includes('.whl') ? '(large file)' : '';
const fileSize =
fileName.includes('.wasm') || fileName.includes('.whl')
? '(large file)'
: '';
// console.log(` ✓ Cached: ${fileName} ${fileSize}`);
} catch (error) {
console.warn('[ServiceWorker] Failed to cache:', url, error.message);

View File

@@ -1,5 +1,4 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
// Supported languages
@@ -48,7 +47,6 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
const langMatch = path.match(
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(?:\/|$)/
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(?:\/|$)/
);
if (
langMatch &&
@@ -75,28 +73,25 @@ export const initI18n = async (): Promise<typeof i18next> => {
const currentLang = getLanguageFromUrl();
await i18next
.use(HttpBackend)
.use(LanguageDetector)
.init({
localStorage.setItem('i18nextLng', currentLang);
await i18next.use(HttpBackend).init({
lng: currentLang,
fallbackLng: 'en',
supportedLngs: supportedLanguages as unknown as string[],
ns: ['common', 'tools'],
defaultNS: 'common',
preload: [currentLang],
backend: {
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
},
detection: {
order: ['path', 'localStorage', 'navigator'],
lookupFromPathIndex: 0,
caches: ['localStorage'],
},
interpolation: {
escapeValue: false,
},
});
await i18next.loadNamespaces('tools');
initialized = true;
return i18next;
};
@@ -122,7 +117,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
let pagePathWithoutLang = relativePath;
const langPrefixMatch = relativePath.match(
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(\/.*)?$/
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(\/.*)?$/
);
if (langPrefixMatch) {
pagePathWithoutLang = langPrefixMatch[2] || '/';
@@ -237,7 +232,7 @@ export const rewriteLinks = (): void => {
newHref = `/${currentLang}/`;
}
} else {
newHref = `${currentLang}/${href}`;
newHref = `/${currentLang}/${href}`;
}
newHref = newHref.replace(/([^:])\/+/g, '$1/');

View File

@@ -1,6 +1,9 @@
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();
/**
* Configuration for progressive rendering
@@ -25,7 +28,12 @@ interface PageTask {
fileName?: string;
container: HTMLElement;
scale?: number;
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement;
createWrapper: (
canvas: HTMLCanvasElement,
pageNumber: number,
fileName?: string
) => HTMLElement;
placeholderElement?: HTMLElement;
}
/**
@@ -34,6 +42,7 @@ interface PageTask {
interface LazyLoadState {
observer: IntersectionObserver | null;
pendingTasks: Map<HTMLElement, PageTask>;
pendingTasksByPageNumber: Map<number, PageTask>;
isRendering: boolean;
eagerLoadQueue: PageTask[];
nextEagerIndex: number;
@@ -42,6 +51,7 @@ interface LazyLoadState {
const lazyLoadState: LazyLoadState = {
observer: null,
pendingTasks: new Map(),
pendingTasksByPageNumber: new Map(),
isRendering: false,
eagerLoadQueue: [],
nextEagerIndex: 0,
@@ -50,7 +60,10 @@ const lazyLoadState: LazyLoadState = {
/**
* Creates a placeholder element for a page that will be lazy-loaded
*/
export function createPlaceholder(pageNumber: number, fileName?: string): HTMLElement {
export function createPlaceholder(
pageNumber: number,
fileName?: string
): HTMLElement {
const placeholder = document.createElement('div');
placeholder.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors';
@@ -62,7 +75,8 @@ export function createPlaceholder(pageNumber: number, fileName?: string): HTMLEl
// Create skeleton loader
const skeletonContainer = document.createElement('div');
skeletonContainer.className = 'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
skeletonContainer.className =
'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
const loadingText = document.createElement('span');
loadingText.className = 'text-gray-500 text-xs';
@@ -107,7 +121,7 @@ async function renderPageBatch(
tasks: PageTask[],
onProgress?: (current: number, total: number) => void
): Promise<void> {
const renderPromises = tasks.map(async (task) => {
for (const task of tasks) {
try {
const canvas = await renderPageToCanvas(
task.pdfjsDoc,
@@ -115,30 +129,50 @@ async function renderPageBatch(
task.scale || 0.5
);
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName);
// Find and replace the placeholder for this specific page number
const placeholder = task.container.querySelector(
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
const wrapper = task.createWrapper(
canvas,
task.pageNumber,
task.fileName
);
if (placeholder) {
// Replace placeholder with rendered page
task.container.replaceChild(wrapper, placeholder);
let placeholder: Element | null = task.placeholderElement || null;
if (!placeholder) {
placeholder = task.container.querySelector(
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
);
}
if (placeholder && placeholder.parentNode) {
const parent = placeholder.parentNode;
parent.insertBefore(wrapper, placeholder);
parent.removeChild(placeholder);
} else {
const allChildren = Array.from(
task.container.children
) as HTMLElement[];
let insertBefore: Element | null = null;
for (const child of allChildren) {
const childPageNum = parseInt(child.dataset.pageNumber || '0', 10);
if (childPageNum > task.pageNumber) {
insertBefore = child;
break;
}
}
if (insertBefore) {
task.container.insertBefore(wrapper, insertBefore);
} else {
// Fallback: shouldn't happen with new approach, but just in case
console.warn(`No placeholder found for page ${task.pageNumber}, appending instead`);
task.container.appendChild(wrapper);
}
return wrapper;
console.warn(
`Placeholder not found for page ${task.pageNumber}, inserted at calculated position`
);
}
} catch (error) {
console.error(`Error rendering page ${task.pageNumber}:`, error);
return null;
}
});
await Promise.all(renderPromises);
}
}
/**
@@ -158,12 +192,19 @@ function setupLazyRendering(
entries.forEach((entry) => {
if (entry.isIntersecting) {
const placeholder = entry.target as HTMLElement;
const task = lazyLoadState.pendingTasks.get(placeholder);
const pageNumberStr = placeholder.dataset.pageNumber;
if (!pageNumberStr) return;
const pageNumber = parseInt(pageNumberStr, 10);
const task = lazyLoadState.pendingTasksByPageNumber.get(pageNumber);
if (task) {
// Immediately unobserve to prevent multiple triggers
observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
lazyLoadState.pendingTasksByPageNumber.delete(pageNumber);
task.placeholderElement = placeholder;
// Render this page immediately (not waiting for isRendering flag)
renderPageBatch([task], config.onProgress)
@@ -174,13 +215,19 @@ function setupLazyRendering(
}
// Check if all pages are rendered
if (lazyLoadState.pendingTasks.size === 0 && lazyLoadState.observer) {
if (
lazyLoadState.pendingTasks.size === 0 &&
lazyLoadState.observer
) {
lazyLoadState.observer.disconnect();
lazyLoadState.observer = null;
}
})
.catch((error) => {
console.error(`Error lazy loading page ${task.pageNumber}:`, error);
console.error(
`Error lazy loading page ${task.pageNumber}:`,
error
);
});
}
}
@@ -208,7 +255,11 @@ function requestIdleCallbackPolyfill(callback: () => void): void {
export async function renderPagesProgressively(
pdfjsDoc: any,
container: HTMLElement,
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement,
createWrapper: (
canvas: HTMLCanvasElement,
pageNumber: number,
fileName?: string
) => HTMLElement,
config: RenderConfig = {}
): Promise<void> {
const {
@@ -236,7 +287,7 @@ export async function renderPagesProgressively(
const tasks: PageTask[] = [];
// Create tasks for all pages
// Create tasks for all pages with direct placeholder references
for (let i = 1; i <= totalPages; i++) {
tasks.push({
pageNumber: i,
@@ -244,6 +295,7 @@ export async function renderPagesProgressively(
container,
scale: config.useLazyLoading ? 0.3 : 0.5,
createWrapper,
placeholderElement: placeholders[i - 1],
});
}
@@ -253,15 +305,17 @@ export async function renderPagesProgressively(
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
const placeholder = placeholders[i - 1];
const task = tasks[i - 1];
// Store the task for lazy rendering
lazyLoadState.pendingTasks.set(placeholder, tasks[i - 1]);
lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.pendingTasksByPageNumber.set(task.pageNumber, task);
observer.observe(placeholder);
}
// Prepare eager load queue
const eagerStartIndex = initialRenderCount;
const eagerEndIndex = Math.min(
eagerStartIndex + (eagerLoadBatches * batchSize),
eagerStartIndex + eagerLoadBatches * batchSize,
totalPages
);
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
@@ -294,7 +348,11 @@ export async function renderPagesProgressively(
}
// Start eager loading AFTER initial batch is complete
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) {
if (
useLazyLoading &&
eagerLoadBatches > 0 &&
totalPages > initialRenderCount
) {
renderEagerBatch(config);
}
}
@@ -311,6 +369,7 @@ export function observePlaceholder(
return;
}
lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.pendingTasksByPageNumber.set(task.pageNumber, task);
lazyLoadState.observer.observe(placeholder);
}
@@ -339,12 +398,12 @@ function renderEagerBatch(config: RenderConfig): void {
if (config.shouldCancel?.()) return;
// Remove these tasks from pending since we're rendering them eagerly
batch.forEach(task => {
const placeholder = Array.from(lazyLoadState.pendingTasks.entries())
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
batch.forEach((task) => {
const placeholder = task.placeholderElement;
if (placeholder && lazyLoadState.observer) {
lazyLoadState.observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
lazyLoadState.pendingTasksByPageNumber.delete(task.pageNumber);
}
});
@@ -358,7 +417,9 @@ function renderEagerBatch(config: RenderConfig): void {
lazyLoadState.nextEagerIndex = batchEnd;
// Queue next eager batch
const remainingBatches = Math.ceil((eagerLoadQueue.length - batchEnd) / batchSize);
const remainingBatches = Math.ceil(
(eagerLoadQueue.length - batchEnd) / batchSize
);
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
// Continue eager loading if we have more batches within the eager threshold
renderEagerBatch(config);
@@ -375,6 +436,7 @@ export function cleanupLazyRendering(): void {
lazyLoadState.observer = null;
}
lazyLoadState.pendingTasks.clear();
lazyLoadState.pendingTasksByPageNumber.clear();
lazyLoadState.isRendering = false;
lazyLoadState.eagerLoadQueue = [];
lazyLoadState.nextEagerIndex = 0;