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:
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDFs oder Bilder",
|
"pdfOrImages": "PDFs oder Bilder",
|
||||||
"filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.",
|
"filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.",
|
||||||
"addMore": "Weitere Dateien hinzufügen",
|
"addMore": "Weitere Dateien hinzufügen",
|
||||||
"clearAll": "Alle löschen"
|
"clearAll": "Alle löschen",
|
||||||
|
"clearFiles": "Dateien löschen"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Verarbeitung..."
|
"processing": "Verarbeitung..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"files": "Dateien"
|
"files": "Dateien",
|
||||||
|
"close": "Schließen"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDFs or Images",
|
"pdfOrImages": "PDFs or Images",
|
||||||
"filesNeverLeave": "Your files never leave your device.",
|
"filesNeverLeave": "Your files never leave your device.",
|
||||||
"addMore": "Add More Files",
|
"addMore": "Add More Files",
|
||||||
"clearAll": "Clear All"
|
"clearAll": "Clear All",
|
||||||
|
"clearFiles": "Clear Files"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Processing..."
|
"processing": "Processing..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
"files": "Files"
|
"files": "Files",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDFs o Imágenes",
|
"pdfOrImages": "PDFs o Imágenes",
|
||||||
"filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.",
|
"filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.",
|
||||||
"addMore": "Agregar Más Archivos",
|
"addMore": "Agregar Más Archivos",
|
||||||
"clearAll": "Limpiar Todo"
|
"clearAll": "Limpiar Todo",
|
||||||
|
"clearFiles": "Borrar archivos"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Procesando..."
|
"processing": "Procesando..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
"file": "Archivo",
|
"file": "Archivo",
|
||||||
"files": "Archivos"
|
"files": "Archivos",
|
||||||
|
"close": "Cerrar"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF ou images",
|
"pdfOrImages": "PDF ou images",
|
||||||
"filesNeverLeave": "Vos fichiers restent sur votre appareil.",
|
"filesNeverLeave": "Vos fichiers restent sur votre appareil.",
|
||||||
"addMore": "Ajouter d’autres fichiers",
|
"addMore": "Ajouter d’autres fichiers",
|
||||||
"clearAll": "Tout effacer"
|
"clearAll": "Tout effacer",
|
||||||
|
"clearFiles": "Effacer les fichiers"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Traitement en cours..."
|
"processing": "Traitement en cours..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
"file": "Fichier",
|
"file": "Fichier",
|
||||||
"files": "Fichiers"
|
"files": "Fichiers",
|
||||||
|
"close": "Fermer"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF atau Gambar",
|
"pdfOrImages": "PDF atau Gambar",
|
||||||
"filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.",
|
"filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.",
|
||||||
"addMore": "Tambah Lebih Banyak File",
|
"addMore": "Tambah Lebih Banyak File",
|
||||||
"clearAll": "Hapus Semua"
|
"clearAll": "Hapus Semua",
|
||||||
|
"clearFiles": "Hapus file"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Memproses..."
|
"processing": "Memproses..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Kesalahan",
|
"error": "Kesalahan",
|
||||||
"success": "Berhasil",
|
"success": "Berhasil",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
"files": "File"
|
"files": "File",
|
||||||
|
"close": "Tutup"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF o immagini",
|
"pdfOrImages": "PDF o immagini",
|
||||||
"filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.",
|
"filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.",
|
||||||
"addMore": "Aggiungi altri file",
|
"addMore": "Aggiungi altri file",
|
||||||
"clearAll": "Svuota tutto"
|
"clearAll": "Svuota tutto",
|
||||||
|
"clearFiles": "Cancella file"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Elaborazione..."
|
"processing": "Elaborazione..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
"success": "Successo",
|
"success": "Successo",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
"files": "File"
|
"files": "File",
|
||||||
|
"close": "Chiudi"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"toolsLabel": "Tools",
|
"toolsLabel": "Tools",
|
||||||
"subtitle": "Klik een tool om de bestandslader te openen",
|
"subtitle": "Klik een tool om de bestandslader te openen",
|
||||||
"searchPlaceholder": "Zoek een tool (bijv., 'splitsen', 'organiseren'...)",
|
"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."
|
"firstLoadNotice": "De eerste keer duurt het even om onze conversiemachine te laden. Daarna gaat alles meteen."
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF's of Afbeeldingen",
|
"pdfOrImages": "PDF's of Afbeeldingen",
|
||||||
"filesNeverLeave": "Je bestanden blijven op jouw apparaat.",
|
"filesNeverLeave": "Je bestanden blijven op jouw apparaat.",
|
||||||
"addMore": "Meer bestanden toevoegen",
|
"addMore": "Meer bestanden toevoegen",
|
||||||
"clearAll": "Alles wissen"
|
"clearAll": "Alles wissen",
|
||||||
|
"clearFiles": "Bestanden wissen"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Verwerken..."
|
"processing": "Verwerken..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"file": "Bestand",
|
"file": "Bestand",
|
||||||
"files": "Bestanden"
|
"files": "Bestanden",
|
||||||
|
"close": "Sluiten"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
@@ -321,4 +323,3 @@
|
|||||||
"failedToLoad": "Laden is mislukt"
|
"failedToLoad": "Laden is mislukt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDFs ou Imagens",
|
"pdfOrImages": "PDFs ou Imagens",
|
||||||
"filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.",
|
"filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.",
|
||||||
"addMore": "Adicionar Mais Arquivos",
|
"addMore": "Adicionar Mais Arquivos",
|
||||||
"clearAll": "Limpar Tudo"
|
"clearAll": "Limpar Tudo",
|
||||||
|
"clearFiles": "Limpar arquivos"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Processando..."
|
"processing": "Processando..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"success": "Sucesso",
|
"success": "Sucesso",
|
||||||
"file": "Arquivo",
|
"file": "Arquivo",
|
||||||
"files": "Arquivos"
|
"files": "Arquivos",
|
||||||
|
"close": "Fechar"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF veya Görseller",
|
"pdfOrImages": "PDF veya Görseller",
|
||||||
"filesNeverLeave": "Dosyalarınız cihazınızı asla terk etmez.",
|
"filesNeverLeave": "Dosyalarınız cihazınızı asla terk etmez.",
|
||||||
"addMore": "Daha Fazla Dosya Ekle",
|
"addMore": "Daha Fazla Dosya Ekle",
|
||||||
"clearAll": "Tümünü Temizle"
|
"clearAll": "Tümünü Temizle",
|
||||||
|
"clearFiles": "Dosyaları temizle"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "İşleniyor..."
|
"processing": "İşleniyor..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Hata",
|
"error": "Hata",
|
||||||
"success": "Başarılı",
|
"success": "Başarılı",
|
||||||
"file": "Dosya",
|
"file": "Dosya",
|
||||||
"files": "Dosya"
|
"files": "Dosya",
|
||||||
|
"close": "Kapat"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF hoặc Hình ảnh",
|
"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ị.",
|
"filesNeverLeave": "Tệp của bạn không bao giờ rời khỏi thiết bị.",
|
||||||
"addMore": "Thêm tệp",
|
"addMore": "Thêm tệp",
|
||||||
"clearAll": "Xóa tất cả"
|
"clearAll": "Xóa tất cả",
|
||||||
|
"clearFiles": "Xóa tệp"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "Đang xử lý..."
|
"processing": "Đang xử lý..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "Lỗi",
|
"error": "Lỗi",
|
||||||
"success": "Thành công",
|
"success": "Thành công",
|
||||||
"file": "Tệp",
|
"file": "Tệp",
|
||||||
"files": "Tệp"
|
"files": "Tệp",
|
||||||
|
"close": "Đóng"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF 或圖片",
|
"pdfOrImages": "PDF 或圖片",
|
||||||
"filesNeverLeave": "你的檔案永遠不會離開你的裝置。",
|
"filesNeverLeave": "你的檔案永遠不會離開你的裝置。",
|
||||||
"addMore": "添加更多檔案",
|
"addMore": "添加更多檔案",
|
||||||
"clearAll": "清除全部"
|
"clearAll": "清除全部",
|
||||||
|
"clearFiles": "清除檔案"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "正在處理..."
|
"processing": "正在處理..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "錯誤",
|
"error": "錯誤",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"file": "檔案",
|
"file": "檔案",
|
||||||
"files": "檔案"
|
"files": "檔案",
|
||||||
|
"close": "關閉"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"pdfOrImages": "PDF 或图片",
|
"pdfOrImages": "PDF 或图片",
|
||||||
"filesNeverLeave": "您的文件从未离开您的设备。",
|
"filesNeverLeave": "您的文件从未离开您的设备。",
|
||||||
"addMore": "添加更多文件",
|
"addMore": "添加更多文件",
|
||||||
"clearAll": "清空所有"
|
"clearAll": "清空所有",
|
||||||
|
"clearFiles": "清除文件"
|
||||||
},
|
},
|
||||||
"loader": {
|
"loader": {
|
||||||
"processing": "处理中..."
|
"processing": "处理中..."
|
||||||
@@ -226,7 +227,8 @@
|
|||||||
"error": "错误",
|
"error": "错误",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
"files": "文件"
|
"files": "文件",
|
||||||
|
"close": "关闭"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
46
public/sw.js
46
public/sw.js
@@ -5,10 +5,9 @@
|
|||||||
* Version: 1.1.0
|
* Version: 1.1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_VERSION = 'bentopdf-v7';
|
const CACHE_VERSION = 'bentopdf-v8';
|
||||||
const CACHE_NAME = `${CACHE_VERSION}-static`;
|
const CACHE_NAME = `${CACHE_VERSION}-static`;
|
||||||
|
|
||||||
|
|
||||||
const getBasePath = () => {
|
const getBasePath = () => {
|
||||||
const scope = self.registration?.scope || self.location.href;
|
const scope = self.registration?.scope || self.location.href;
|
||||||
const url = new URL(scope);
|
const url = new URL(scope);
|
||||||
@@ -44,7 +43,8 @@ self.addEventListener('install', (event) => {
|
|||||||
// console.log('📦 [ServiceWorker] Will cache', CRITICAL_ASSETS.length, 'critical assets');
|
// console.log('📦 [ServiceWorker] Will cache', CRITICAL_ASSETS.length, 'critical assets');
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME)
|
caches
|
||||||
|
.open(CACHE_NAME)
|
||||||
.then((cache) => {
|
.then((cache) => {
|
||||||
// console.log('[ServiceWorker] Caching critical assets...');
|
// console.log('[ServiceWorker] Caching critical assets...');
|
||||||
return cacheInBatches(cache, CRITICAL_ASSETS, 5);
|
return cacheInBatches(cache, CRITICAL_ASSETS, 5);
|
||||||
@@ -64,7 +64,8 @@ self.addEventListener('activate', (event) => {
|
|||||||
// console.log('🔄 [ServiceWorker] Activating version:', CACHE_VERSION);
|
// console.log('🔄 [ServiceWorker] Activating version:', CACHE_VERSION);
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.keys()
|
caches
|
||||||
|
.keys()
|
||||||
.then((cacheNames) => {
|
.then((cacheNames) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
cacheNames.map((cacheName) => {
|
cacheNames.map((cacheName) => {
|
||||||
@@ -92,18 +93,33 @@ self.addEventListener('fetch', (event) => {
|
|||||||
if (!isLocal && !isCDN) {
|
if (!isLocal && !isCDN) {
|
||||||
return;
|
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);
|
// console.log('🔧 [Dev Mode] Skipping Vite HMR request:', url.pathname);
|
||||||
return;
|
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;
|
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));
|
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));
|
event.respondWith(networkFirstStrategy(event.request));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -154,7 +170,10 @@ async function cacheFirstStrategyWithDedup(request, isCDN) {
|
|||||||
return fallbackResponse;
|
return fallbackResponse;
|
||||||
}
|
}
|
||||||
} catch (fallbackError) {
|
} 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('/ghostscript-wasm/') ||
|
||||||
pathname.includes('/embedpdf/') ||
|
pathname.includes('/embedpdf/') ||
|
||||||
pathname.includes('/assets/') ||
|
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 {
|
try {
|
||||||
await cache.add(url);
|
await cache.add(url);
|
||||||
const fileName = url.split('/').pop();
|
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}`);
|
// console.log(` ✓ Cached: ${fileName} ${fileSize}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ServiceWorker] Failed to cache:', url, error.message);
|
console.warn('[ServiceWorker] Failed to cache:', url, error.message);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
||||||
import HttpBackend from 'i18next-http-backend';
|
import HttpBackend from 'i18next-http-backend';
|
||||||
|
|
||||||
// Supported languages
|
// Supported languages
|
||||||
@@ -48,7 +47,6 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
|||||||
|
|
||||||
const langMatch = path.match(
|
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|nl)(?:\/|$)/
|
||||||
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(?:\/|$)/
|
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
langMatch &&
|
langMatch &&
|
||||||
@@ -75,28 +73,25 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
|||||||
|
|
||||||
const currentLang = getLanguageFromUrl();
|
const currentLang = getLanguageFromUrl();
|
||||||
|
|
||||||
await i18next
|
localStorage.setItem('i18nextLng', currentLang);
|
||||||
.use(HttpBackend)
|
|
||||||
.use(LanguageDetector)
|
await i18next.use(HttpBackend).init({
|
||||||
.init({
|
|
||||||
lng: currentLang,
|
lng: currentLang,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
supportedLngs: supportedLanguages as unknown as string[],
|
supportedLngs: supportedLanguages as unknown as string[],
|
||||||
ns: ['common', 'tools'],
|
ns: ['common', 'tools'],
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
preload: [currentLang],
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
|
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
|
||||||
},
|
},
|
||||||
detection: {
|
|
||||||
order: ['path', 'localStorage', 'navigator'],
|
|
||||||
lookupFromPathIndex: 0,
|
|
||||||
caches: ['localStorage'],
|
|
||||||
},
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await i18next.loadNamespaces('tools');
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
return i18next;
|
return i18next;
|
||||||
};
|
};
|
||||||
@@ -122,7 +117,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
|
|||||||
|
|
||||||
let pagePathWithoutLang = relativePath;
|
let pagePathWithoutLang = relativePath;
|
||||||
const langPrefixMatch = relativePath.match(
|
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) {
|
if (langPrefixMatch) {
|
||||||
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
||||||
@@ -237,7 +232,7 @@ export const rewriteLinks = (): void => {
|
|||||||
newHref = `/${currentLang}/`;
|
newHref = `/${currentLang}/`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newHref = `${currentLang}/${href}`;
|
newHref = `/${currentLang}/${href}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
newHref = newHref.replace(/([^:])\/+/g, '$1/');
|
newHref = newHref.replace(/([^:])\/+/g, '$1/');
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
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
|
* Configuration for progressive rendering
|
||||||
@@ -25,7 +28,12 @@ interface PageTask {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
scale?: number;
|
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 {
|
interface LazyLoadState {
|
||||||
observer: IntersectionObserver | null;
|
observer: IntersectionObserver | null;
|
||||||
pendingTasks: Map<HTMLElement, PageTask>;
|
pendingTasks: Map<HTMLElement, PageTask>;
|
||||||
|
pendingTasksByPageNumber: Map<number, PageTask>;
|
||||||
isRendering: boolean;
|
isRendering: boolean;
|
||||||
eagerLoadQueue: PageTask[];
|
eagerLoadQueue: PageTask[];
|
||||||
nextEagerIndex: number;
|
nextEagerIndex: number;
|
||||||
@@ -42,6 +51,7 @@ interface LazyLoadState {
|
|||||||
const lazyLoadState: LazyLoadState = {
|
const lazyLoadState: LazyLoadState = {
|
||||||
observer: null,
|
observer: null,
|
||||||
pendingTasks: new Map(),
|
pendingTasks: new Map(),
|
||||||
|
pendingTasksByPageNumber: new Map(),
|
||||||
isRendering: false,
|
isRendering: false,
|
||||||
eagerLoadQueue: [],
|
eagerLoadQueue: [],
|
||||||
nextEagerIndex: 0,
|
nextEagerIndex: 0,
|
||||||
@@ -50,7 +60,10 @@ const lazyLoadState: LazyLoadState = {
|
|||||||
/**
|
/**
|
||||||
* Creates a placeholder element for a page that will be lazy-loaded
|
* 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');
|
const placeholder = document.createElement('div');
|
||||||
placeholder.className =
|
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';
|
'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
|
// Create skeleton loader
|
||||||
const skeletonContainer = document.createElement('div');
|
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');
|
const loadingText = document.createElement('span');
|
||||||
loadingText.className = 'text-gray-500 text-xs';
|
loadingText.className = 'text-gray-500 text-xs';
|
||||||
@@ -107,7 +121,7 @@ async function renderPageBatch(
|
|||||||
tasks: PageTask[],
|
tasks: PageTask[],
|
||||||
onProgress?: (current: number, total: number) => void
|
onProgress?: (current: number, total: number) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const renderPromises = tasks.map(async (task) => {
|
for (const task of tasks) {
|
||||||
try {
|
try {
|
||||||
const canvas = await renderPageToCanvas(
|
const canvas = await renderPageToCanvas(
|
||||||
task.pdfjsDoc,
|
task.pdfjsDoc,
|
||||||
@@ -115,30 +129,50 @@ async function renderPageBatch(
|
|||||||
task.scale || 0.5
|
task.scale || 0.5
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName);
|
const wrapper = task.createWrapper(
|
||||||
|
canvas,
|
||||||
// Find and replace the placeholder for this specific page number
|
task.pageNumber,
|
||||||
const placeholder = task.container.querySelector(
|
task.fileName
|
||||||
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (placeholder) {
|
let placeholder: Element | null = task.placeholderElement || null;
|
||||||
// Replace placeholder with rendered page
|
if (!placeholder) {
|
||||||
task.container.replaceChild(wrapper, 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 {
|
} 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);
|
task.container.appendChild(wrapper);
|
||||||
}
|
}
|
||||||
|
console.warn(
|
||||||
return wrapper;
|
`Placeholder not found for page ${task.pageNumber}, inserted at calculated position`
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error rendering page ${task.pageNumber}:`, error);
|
console.error(`Error rendering page ${task.pageNumber}:`, error);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
await Promise.all(renderPromises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,12 +192,19 @@ function setupLazyRendering(
|
|||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const placeholder = entry.target as HTMLElement;
|
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) {
|
if (task) {
|
||||||
// Immediately unobserve to prevent multiple triggers
|
// Immediately unobserve to prevent multiple triggers
|
||||||
observer.unobserve(placeholder);
|
observer.unobserve(placeholder);
|
||||||
lazyLoadState.pendingTasks.delete(placeholder);
|
lazyLoadState.pendingTasks.delete(placeholder);
|
||||||
|
lazyLoadState.pendingTasksByPageNumber.delete(pageNumber);
|
||||||
|
|
||||||
|
task.placeholderElement = placeholder;
|
||||||
|
|
||||||
// Render this page immediately (not waiting for isRendering flag)
|
// Render this page immediately (not waiting for isRendering flag)
|
||||||
renderPageBatch([task], config.onProgress)
|
renderPageBatch([task], config.onProgress)
|
||||||
@@ -174,13 +215,19 @@ function setupLazyRendering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if all pages are rendered
|
// 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.disconnect();
|
||||||
lazyLoadState.observer = null;
|
lazyLoadState.observer = null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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(
|
export async function renderPagesProgressively(
|
||||||
pdfjsDoc: any,
|
pdfjsDoc: any,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement,
|
createWrapper: (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
pageNumber: number,
|
||||||
|
fileName?: string
|
||||||
|
) => HTMLElement,
|
||||||
config: RenderConfig = {}
|
config: RenderConfig = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const {
|
const {
|
||||||
@@ -236,7 +287,7 @@ export async function renderPagesProgressively(
|
|||||||
|
|
||||||
const tasks: PageTask[] = [];
|
const tasks: PageTask[] = [];
|
||||||
|
|
||||||
// Create tasks for all pages
|
// Create tasks for all pages with direct placeholder references
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
@@ -244,6 +295,7 @@ export async function renderPagesProgressively(
|
|||||||
container,
|
container,
|
||||||
scale: config.useLazyLoading ? 0.3 : 0.5,
|
scale: config.useLazyLoading ? 0.3 : 0.5,
|
||||||
createWrapper,
|
createWrapper,
|
||||||
|
placeholderElement: placeholders[i - 1],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,15 +305,17 @@ export async function renderPagesProgressively(
|
|||||||
|
|
||||||
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
|
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
|
||||||
const placeholder = placeholders[i - 1];
|
const placeholder = placeholders[i - 1];
|
||||||
|
const task = tasks[i - 1];
|
||||||
// Store the task for lazy rendering
|
// 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);
|
observer.observe(placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare eager load queue
|
// Prepare eager load queue
|
||||||
const eagerStartIndex = initialRenderCount;
|
const eagerStartIndex = initialRenderCount;
|
||||||
const eagerEndIndex = Math.min(
|
const eagerEndIndex = Math.min(
|
||||||
eagerStartIndex + (eagerLoadBatches * batchSize),
|
eagerStartIndex + eagerLoadBatches * batchSize,
|
||||||
totalPages
|
totalPages
|
||||||
);
|
);
|
||||||
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
|
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
|
||||||
@@ -294,7 +348,11 @@ export async function renderPagesProgressively(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start eager loading AFTER initial batch is complete
|
// Start eager loading AFTER initial batch is complete
|
||||||
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) {
|
if (
|
||||||
|
useLazyLoading &&
|
||||||
|
eagerLoadBatches > 0 &&
|
||||||
|
totalPages > initialRenderCount
|
||||||
|
) {
|
||||||
renderEagerBatch(config);
|
renderEagerBatch(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +369,7 @@ export function observePlaceholder(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lazyLoadState.pendingTasks.set(placeholder, task);
|
lazyLoadState.pendingTasks.set(placeholder, task);
|
||||||
|
lazyLoadState.pendingTasksByPageNumber.set(task.pageNumber, task);
|
||||||
lazyLoadState.observer.observe(placeholder);
|
lazyLoadState.observer.observe(placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,12 +398,12 @@ function renderEagerBatch(config: RenderConfig): void {
|
|||||||
if (config.shouldCancel?.()) return;
|
if (config.shouldCancel?.()) return;
|
||||||
|
|
||||||
// Remove these tasks from pending since we're rendering them eagerly
|
// Remove these tasks from pending since we're rendering them eagerly
|
||||||
batch.forEach(task => {
|
batch.forEach((task) => {
|
||||||
const placeholder = Array.from(lazyLoadState.pendingTasks.entries())
|
const placeholder = task.placeholderElement;
|
||||||
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
|
|
||||||
if (placeholder && lazyLoadState.observer) {
|
if (placeholder && lazyLoadState.observer) {
|
||||||
lazyLoadState.observer.unobserve(placeholder);
|
lazyLoadState.observer.unobserve(placeholder);
|
||||||
lazyLoadState.pendingTasks.delete(placeholder);
|
lazyLoadState.pendingTasks.delete(placeholder);
|
||||||
|
lazyLoadState.pendingTasksByPageNumber.delete(task.pageNumber);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,7 +417,9 @@ function renderEagerBatch(config: RenderConfig): void {
|
|||||||
lazyLoadState.nextEagerIndex = batchEnd;
|
lazyLoadState.nextEagerIndex = batchEnd;
|
||||||
|
|
||||||
// Queue next eager batch
|
// 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) {
|
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
|
||||||
// Continue eager loading if we have more batches within the eager threshold
|
// Continue eager loading if we have more batches within the eager threshold
|
||||||
renderEagerBatch(config);
|
renderEagerBatch(config);
|
||||||
@@ -375,6 +436,7 @@ export function cleanupLazyRendering(): void {
|
|||||||
lazyLoadState.observer = null;
|
lazyLoadState.observer = null;
|
||||||
}
|
}
|
||||||
lazyLoadState.pendingTasks.clear();
|
lazyLoadState.pendingTasks.clear();
|
||||||
|
lazyLoadState.pendingTasksByPageNumber.clear();
|
||||||
lazyLoadState.isRendering = false;
|
lazyLoadState.isRendering = false;
|
||||||
lazyLoadState.eagerLoadQueue = [];
|
lazyLoadState.eagerLoadQueue = [];
|
||||||
lazyLoadState.nextEagerIndex = 0;
|
lazyLoadState.nextEagerIndex = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user