diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 0bf85306..0615a53f 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1548,5 +1548,19 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "هل أنت متأكد من مسح التمييزات والملاحظات الـ{{count}}؟", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "اتركه فارغًا لاستخدام واجهة Readwise الرسمية. عيّن عنوان URL مخصصًا فقط للاتصال بخدمة مستضافة ذاتيًا ومتوافقة مع Readwise.", - "Custom URL": "عنوان URL مخصص" + "Custom URL": "عنوان URL مخصص", + "Book is not ready yet, please try again.": "الكتاب غير جاهز بعد، يرجى المحاولة مرة أخرى.", + "Select Moon+ Reader Export File": "اختر ملف تصدير Moon+ Reader", + "Failed to read the selected file.": "فشل قراءة الملف المحدد.", + "No annotations found in the file.": "لم يتم العثور على أي تعليقات في الملف.", + "Failed to import annotations.": "فشل استيراد التعليقات.", + "No annotations could be located in this book.": "تعذّر تحديد موضع أي تعليقات في هذا الكتاب.", + "Imported {{count}} annotations_zero": "لم يتم استيراد أي تعليقات", + "Imported {{count}} annotations_one": "تم استيراد تعليق واحد", + "Imported {{count}} annotations_two": "تم استيراد تعليقين", + "Imported {{count}} annotations_few": "تم استيراد {{count}} تعليقات", + "Imported {{count}} annotations_many": "تم استيراد {{count}} تعليقًا", + "Imported {{count}} annotations_other": "تم استيراد {{count}} تعليق", + "No new annotations to import": "لا توجد تعليقات جديدة للاستيراد", + "Import from Moon+ Reader": "استيراد من Moon+ Reader" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index feff091b..a3ba092f 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "আপনি কি নিশ্চিত যে সব {{count}}টি হাইলাইট ও নোট মুছে ফেলতে চান?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "অফিসিয়াল Readwise API ব্যবহার করতে ফাঁকা রাখুন। শুধুমাত্র একটি স্ব-হোস্ট করা, Readwise-সামঞ্জস্যপূর্ণ পরিষেবার জন্য একটি কাস্টম URL সেট করুন।", - "Custom URL": "কাস্টম URL" + "Custom URL": "কাস্টম URL", + "Book is not ready yet, please try again.": "বইটি এখনও প্রস্তুত নয়, অনুগ্রহ করে আবার চেষ্টা করুন।", + "Select Moon+ Reader Export File": "Moon+ Reader এক্সপোর্ট ফাইল নির্বাচন করুন", + "Failed to read the selected file.": "নির্বাচিত ফাইলটি পড়তে ব্যর্থ হয়েছে।", + "No annotations found in the file.": "ফাইলে কোনো টীকা পাওয়া যায়নি।", + "Failed to import annotations.": "টীকা ইম্পোর্ট করতে ব্যর্থ হয়েছে।", + "No annotations could be located in this book.": "এই বইয়ে কোনো টীকার অবস্থান নির্ণয় করা যায়নি।", + "Imported {{count}} annotations_one": "{{count}}টি টীকা ইম্পোর্ট করা হয়েছে", + "Imported {{count}} annotations_other": "{{count}}টি টীকা ইম্পোর্ট করা হয়েছে", + "No new annotations to import": "ইম্পোর্ট করার জন্য কোনো নতুন টীকা নেই", + "Import from Moon+ Reader": "Moon+ Reader থেকে ইম্পোর্ট করুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 8c665058..fa4856ec 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "གཙོ་གནད་དང་ཟིན་བྲིས་ {{count}} ཚང་མ་སུབ་པར་གཏན་འཁེལ་ལམ།", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Readwise ཡི་ངོ་མའི་API སྤྱོད་ན་སྟོང་པར་བཞག Readwise དང་མཐུན་པའི་རང་འཇོག་ཞབས་ཞུའི་ཆེད་ཁོ་ནར་སྒེར་གྱི་URL འགོད་དགོས།", - "Custom URL": "སྒེར་གྱི་URL" + "Custom URL": "སྒེར་གྱི་URL", + "Book is not ready yet, please try again.": "དཔེ་ཆ་ད་དུང་གྲ་སྒྲིག་མ་ཟིན་པས། ཡང་བསྐྱར་ཚོད་ལྟ་གནང་རོགས།", + "Select Moon+ Reader Export File": "Moon+ Reader ཕྱིར་འདྲེན་ཡིག་ཆ་འདེམས།", + "Failed to read the selected file.": "འདེམས་ཟིན་པའི་ཡིག་ཆ་ཀློག་མ་ཐུབ།", + "No annotations found in the file.": "ཡིག་ཆའི་ནང་མཆན་འགྲེལ་མ་རྙེད།", + "Failed to import annotations.": "མཆན་འགྲེལ་ནང་འདྲེན་མ་ཐུབ།", + "No annotations could be located in this book.": "དཔེ་ཆ་འདིའི་ནང་མཆན་འགྲེལ་གང་ཡང་གནས་ས་ངོས་འཛིན་མ་ཐུབ།", + "Imported {{count}} annotations_other": "མཆན་འགྲེལ་ {{count}} ནང་འདྲེན་བྱས་ཟིན།", + "No new annotations to import": "ནང་འདྲེན་བྱ་རྒྱུའི་མཆན་འགྲེལ་གསར་པ་མེད།", + "Import from Moon+ Reader": "Moon+ Reader ནས་ནང་འདྲེན།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 4d78d767..1f3e8d62 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Möchten Sie wirklich alle {{count}} Markierungen und Notizen löschen?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Leer lassen, um die offizielle Readwise-API zu verwenden. Eine benutzerdefinierte URL nur festlegen, um einen selbst gehosteten, Readwise-kompatiblen Dienst anzusprechen.", - "Custom URL": "Benutzerdefinierte URL" + "Custom URL": "Benutzerdefinierte URL", + "Book is not ready yet, please try again.": "Das Buch ist noch nicht bereit, bitte versuche es erneut.", + "Select Moon+ Reader Export File": "Moon+ Reader-Exportdatei auswählen", + "Failed to read the selected file.": "Die ausgewählte Datei konnte nicht gelesen werden.", + "No annotations found in the file.": "Keine Anmerkungen in der Datei gefunden.", + "Failed to import annotations.": "Anmerkungen konnten nicht importiert werden.", + "No annotations could be located in this book.": "In diesem Buch konnten keine Anmerkungen zugeordnet werden.", + "Imported {{count}} annotations_one": "{{count}} Anmerkung importiert", + "Imported {{count}} annotations_other": "{{count}} Anmerkungen importiert", + "No new annotations to import": "Keine neuen Anmerkungen zum Importieren", + "Import from Moon+ Reader": "Aus Moon+ Reader importieren" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 11743f7d..f77a1454 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Είστε σίγουροι ότι θέλετε να διαγράψετε και τις {{count}} επισημάνσεις και σημειώσεις;", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Αφήστε το κενό για χρήση του επίσημου Readwise API. Ορίστε προσαρμοσμένο URL μόνο για να στοχεύσετε μια αυτο-φιλοξενούμενη υπηρεσία συμβατή με το Readwise.", - "Custom URL": "Προσαρμοσμένο URL" + "Custom URL": "Προσαρμοσμένο URL", + "Book is not ready yet, please try again.": "Το βιβλίο δεν είναι ακόμη έτοιμο, δοκιμάστε ξανά.", + "Select Moon+ Reader Export File": "Επιλογή αρχείου εξαγωγής Moon+ Reader", + "Failed to read the selected file.": "Αποτυχία ανάγνωσης του επιλεγμένου αρχείου.", + "No annotations found in the file.": "Δεν βρέθηκαν σχόλια στο αρχείο.", + "Failed to import annotations.": "Αποτυχία εισαγωγής σχολίων.", + "No annotations could be located in this book.": "Δεν ήταν δυνατός ο εντοπισμός σχολίων σε αυτό το βιβλίο.", + "Imported {{count}} annotations_one": "Εισήχθη {{count}} σχόλιο", + "Imported {{count}} annotations_other": "Εισήχθησαν {{count}} σχόλια", + "No new annotations to import": "Δεν υπάρχουν νέα σχόλια για εισαγωγή", + "Import from Moon+ Reader": "Εισαγωγή από Moon+ Reader" } diff --git a/apps/readest-app/public/locales/en/translation.json b/apps/readest-app/public/locales/en/translation.json index c63486bf..1dd660de 100644 --- a/apps/readest-app/public/locales/en/translation.json +++ b/apps/readest-app/public/locales/en/translation.json @@ -57,5 +57,7 @@ "Cleared {{count}} highlights and notes._one": "Cleared {{count}} highlight and note.", "Cleared {{count}} highlights and notes._other": "Cleared {{count}} highlights and notes.", "Are you sure to clear all {{count}} highlights and notes?_one": "Are you sure to clear all {{count}} highlight and note?", - "Are you sure to clear all {{count}} highlights and notes?_other": "Are you sure to clear all {{count}} highlights and notes?" + "Are you sure to clear all {{count}} highlights and notes?_other": "Are you sure to clear all {{count}} highlights and notes?", + "Imported {{count}} annotations_one": "Imported {{count}} annotation", + "Imported {{count}} annotations_other": "Imported {{count}} annotations" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 00b6912a..5cd0db55 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "¿Seguro que quieres borrar los {{count}} resaltados y notas?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Déjalo en blanco para usar la API oficial de Readwise. Establece una URL personalizada solo para apuntar a un servicio autoalojado compatible con Readwise.", - "Custom URL": "URL personalizada" + "Custom URL": "URL personalizada", + "Book is not ready yet, please try again.": "El libro aún no está listo, inténtalo de nuevo.", + "Select Moon+ Reader Export File": "Seleccionar archivo de exportación de Moon+ Reader", + "Failed to read the selected file.": "No se pudo leer el archivo seleccionado.", + "No annotations found in the file.": "No se encontraron anotaciones en el archivo.", + "Failed to import annotations.": "No se pudieron importar las anotaciones.", + "No annotations could be located in this book.": "No se pudo ubicar ninguna anotación en este libro.", + "Imported {{count}} annotations_one": "{{count}} anotación importada", + "Imported {{count}} annotations_many": "{{count}} anotaciones importadas", + "Imported {{count}} annotations_other": "{{count}} anotaciones importadas", + "No new annotations to import": "No hay anotaciones nuevas para importar", + "Import from Moon+ Reader": "Importar desde Moon+ Reader" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 595d4a39..00593752 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "آیا مطمئنید که می‌خواهید همهٔ {{count}} برجسته و یادداشت را پاک کنید؟", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "برای استفاده از API رسمی Readwise خالی بگذارید. تنها برای اتصال به یک سرویس خودمیزبان سازگار با Readwise یک نشانی سفارشی تنظیم کنید.", - "Custom URL": "نشانی سفارشی" + "Custom URL": "نشانی سفارشی", + "Book is not ready yet, please try again.": "کتاب هنوز آماده نیست، لطفاً دوباره تلاش کنید.", + "Select Moon+ Reader Export File": "انتخاب فایل خروجی Moon+ Reader", + "Failed to read the selected file.": "خواندن فایل انتخاب‌شده ناموفق بود.", + "No annotations found in the file.": "هیچ یادداشتی در فایل یافت نشد.", + "Failed to import annotations.": "وارد کردن یادداشت‌ها ناموفق بود.", + "No annotations could be located in this book.": "مکان هیچ یادداشتی در این کتاب پیدا نشد.", + "Imported {{count}} annotations_one": "{{count}} یادداشت وارد شد", + "Imported {{count}} annotations_other": "{{count}} یادداشت وارد شد", + "No new annotations to import": "یادداشت جدیدی برای وارد کردن وجود ندارد", + "Import from Moon+ Reader": "وارد کردن از Moon+ Reader" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index e6dd4f01..da95ed81 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Voulez-vous vraiment effacer les {{count}} surlignages et notes ?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Laissez vide pour utiliser l'API officielle de Readwise. Définissez une URL personnalisée uniquement pour cibler un service auto-hébergé compatible avec Readwise.", - "Custom URL": "URL personnalisée" + "Custom URL": "URL personnalisée", + "Book is not ready yet, please try again.": "Le livre n'est pas encore prêt, veuillez réessayer.", + "Select Moon+ Reader Export File": "Sélectionner le fichier d'exportation Moon+ Reader", + "Failed to read the selected file.": "Échec de la lecture du fichier sélectionné.", + "No annotations found in the file.": "Aucune annotation trouvée dans le fichier.", + "Failed to import annotations.": "Échec de l'importation des annotations.", + "No annotations could be located in this book.": "Impossible de localiser des annotations dans ce livre.", + "Imported {{count}} annotations_one": "{{count}} annotation importée", + "Imported {{count}} annotations_many": "{{count}} annotations importées", + "Imported {{count}} annotations_other": "{{count}} annotations importées", + "No new annotations to import": "Aucune nouvelle annotation à importer", + "Import from Moon+ Reader": "Importer depuis Moon+ Reader" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index cccc4c3c..9b1bd537 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "האם לנקות את כל {{count}} ההדגשות וההערות?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "השאירו ריק כדי להשתמש ב-API הרשמי של Readwise. הגדירו כתובת URL מותאמת אישית רק כדי להפנות לשירות בעל אירוח עצמי התואם ל-Readwise.", - "Custom URL": "כתובת URL מותאמת אישית" + "Custom URL": "כתובת URL מותאמת אישית", + "Book is not ready yet, please try again.": "הספר עדיין לא מוכן, נסה שוב.", + "Select Moon+ Reader Export File": "בחר קובץ ייצוא של Moon+ Reader", + "Failed to read the selected file.": "קריאת הקובץ הנבחר נכשלה.", + "No annotations found in the file.": "לא נמצאו הערות בקובץ.", + "Failed to import annotations.": "ייבוא ההערות נכשל.", + "No annotations could be located in this book.": "לא ניתן היה לאתר הערות בספר זה.", + "Imported {{count}} annotations_one": "יובאה הערה אחת", + "Imported {{count}} annotations_two": "יובאו שתי הערות", + "Imported {{count}} annotations_other": "יובאו {{count}} הערות", + "No new annotations to import": "אין הערות חדשות לייבוא", + "Import from Moon+ Reader": "ייבוא מ-Moon+ Reader" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 162b89bc..705cabd4 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "क्या आप वाकई सभी {{count}} हाइलाइट और नोट साफ़ करना चाहते हैं?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "आधिकारिक Readwise API का उपयोग करने के लिए खाली छोड़ें। कस्टम URL केवल तभी सेट करें जब किसी स्वयं-होस्ट किए गए, Readwise-संगत सेवा को लक्षित करना हो।", - "Custom URL": "कस्टम URL" + "Custom URL": "कस्टम URL", + "Book is not ready yet, please try again.": "पुस्तक अभी तैयार नहीं है, कृपया पुनः प्रयास करें।", + "Select Moon+ Reader Export File": "Moon+ Reader निर्यात फ़ाइल चुनें", + "Failed to read the selected file.": "चयनित फ़ाइल पढ़ने में विफल।", + "No annotations found in the file.": "फ़ाइल में कोई एनोटेशन नहीं मिला।", + "Failed to import annotations.": "एनोटेशन आयात करने में विफल।", + "No annotations could be located in this book.": "इस पुस्तक में कोई एनोटेशन नहीं मिल सका।", + "Imported {{count}} annotations_one": "{{count}} एनोटेशन आयात किया गया", + "Imported {{count}} annotations_other": "{{count}} एनोटेशन आयात किए गए", + "No new annotations to import": "आयात करने के लिए कोई नया एनोटेशन नहीं", + "Import from Moon+ Reader": "Moon+ Reader से आयात करें" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 5bf9c241..32a8f286 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Biztosan törli mind a(z) {{count}} kiemelést és jegyzetet?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Hagyja üresen a hivatalos Readwise API használatához. Egyéni URL-t csak akkor adjon meg, ha saját üzemeltetésű, Readwise-kompatibilis szolgáltatást szeretne megcélozni.", - "Custom URL": "Egyéni URL" + "Custom URL": "Egyéni URL", + "Book is not ready yet, please try again.": "A könyv még nincs kész, próbáld újra.", + "Select Moon+ Reader Export File": "Moon+ Reader exportfájl kiválasztása", + "Failed to read the selected file.": "A kiválasztott fájl beolvasása sikertelen.", + "No annotations found in the file.": "Nem található jegyzet a fájlban.", + "Failed to import annotations.": "A jegyzetek importálása sikertelen.", + "No annotations could be located in this book.": "Nem sikerült jegyzeteket azonosítani ebben a könyvben.", + "Imported {{count}} annotations_one": "{{count}} jegyzet importálva", + "Imported {{count}} annotations_other": "{{count}} jegyzet importálva", + "No new annotations to import": "Nincs új importálható jegyzet", + "Import from Moon+ Reader": "Importálás Moon+ Reader programból" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index ae5f5724..24b24e53 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Yakin ingin menghapus semua {{count}} sorotan dan catatan?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Kosongkan untuk menggunakan API Readwise resmi. Setel URL khusus hanya untuk menargetkan layanan yang dihosting sendiri dan kompatibel dengan Readwise.", - "Custom URL": "URL Khusus" + "Custom URL": "URL Khusus", + "Book is not ready yet, please try again.": "Buku belum siap, silakan coba lagi.", + "Select Moon+ Reader Export File": "Pilih file ekspor Moon+ Reader", + "Failed to read the selected file.": "Gagal membaca file yang dipilih.", + "No annotations found in the file.": "Tidak ada anotasi ditemukan dalam file.", + "Failed to import annotations.": "Gagal mengimpor anotasi.", + "No annotations could be located in this book.": "Tidak ada anotasi yang dapat ditemukan di buku ini.", + "Imported {{count}} annotations_other": "{{count}} anotasi diimpor", + "No new annotations to import": "Tidak ada anotasi baru untuk diimpor", + "Import from Moon+ Reader": "Impor dari Moon+ Reader" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index e0e8c415..5ee098fb 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Vuoi davvero cancellare tutte le {{count}} evidenziazioni e note?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Lascia vuoto per usare l'API ufficiale di Readwise. Imposta un URL personalizzato solo per puntare a un servizio self-hosted compatibile con Readwise.", - "Custom URL": "URL personalizzato" + "Custom URL": "URL personalizzato", + "Book is not ready yet, please try again.": "Il libro non è ancora pronto, riprova.", + "Select Moon+ Reader Export File": "Seleziona il file di esportazione di Moon+ Reader", + "Failed to read the selected file.": "Impossibile leggere il file selezionato.", + "No annotations found in the file.": "Nessuna annotazione trovata nel file.", + "Failed to import annotations.": "Impossibile importare le annotazioni.", + "No annotations could be located in this book.": "Impossibile individuare annotazioni in questo libro.", + "Imported {{count}} annotations_one": "{{count}} annotazione importata", + "Imported {{count}} annotations_many": "{{count}} annotazioni importate", + "Imported {{count}} annotations_other": "{{count}} annotazioni importate", + "No new annotations to import": "Nessuna nuova annotazione da importare", + "Import from Moon+ Reader": "Importa da Moon+ Reader" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index b144dc26..d246226c 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "{{count}} 件のハイライトとメモをすべてクリアしてもよろしいですか?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "公式のReadwise APIを使用する場合は空白のままにします。カスタムURLは、セルフホストのReadwise互換サービスに接続する場合のみ設定してください。", - "Custom URL": "カスタムURL" + "Custom URL": "カスタムURL", + "Book is not ready yet, please try again.": "本の準備がまだできていません。もう一度お試しください。", + "Select Moon+ Reader Export File": "Moon+ Reader のエクスポートファイルを選択", + "Failed to read the selected file.": "選択したファイルを読み込めませんでした。", + "No annotations found in the file.": "ファイルに注釈が見つかりませんでした。", + "Failed to import annotations.": "注釈をインポートできませんでした。", + "No annotations could be located in this book.": "この本で注釈の位置を特定できませんでした。", + "Imported {{count}} annotations_other": "{{count}} 件の注釈をインポートしました", + "No new annotations to import": "インポートする新しい注釈はありません", + "Import from Moon+ Reader": "Moon+ Reader からインポート" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 343b2675..7195f52e 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "하이라이트 및 메모 {{count}}개를 모두 지우시겠습니까?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "공식 Readwise API를 사용하려면 비워 두세요. 사용자 지정 URL은 자체 호스팅된 Readwise 호환 서비스를 대상으로 할 때만 설정하세요.", - "Custom URL": "사용자 지정 URL" + "Custom URL": "사용자 지정 URL", + "Book is not ready yet, please try again.": "책이 아직 준비되지 않았습니다. 다시 시도해 주세요.", + "Select Moon+ Reader Export File": "Moon+ Reader 내보내기 파일 선택", + "Failed to read the selected file.": "선택한 파일을 읽지 못했습니다.", + "No annotations found in the file.": "파일에서 주석을 찾을 수 없습니다.", + "Failed to import annotations.": "주석을 가져오지 못했습니다.", + "No annotations could be located in this book.": "이 책에서 주석의 위치를 찾을 수 없습니다.", + "Imported {{count}} annotations_other": "주석 {{count}}개를 가져왔습니다", + "No new annotations to import": "가져올 새 주석이 없습니다", + "Import from Moon+ Reader": "Moon+ Reader에서 가져오기" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index a1535962..572dfdb3 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Adakah anda pasti mahu mengosongkan semua {{count}} serlahan dan nota?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Biarkan kosong untuk menggunakan API Readwise rasmi. Tetapkan URL tersuai hanya untuk menyasarkan perkhidmatan layan-diri yang serasi dengan Readwise.", - "Custom URL": "URL Tersuai" + "Custom URL": "URL Tersuai", + "Book is not ready yet, please try again.": "Buku belum sedia, sila cuba lagi.", + "Select Moon+ Reader Export File": "Pilih fail eksport Moon+ Reader", + "Failed to read the selected file.": "Gagal membaca fail yang dipilih.", + "No annotations found in the file.": "Tiada anotasi ditemui dalam fail.", + "Failed to import annotations.": "Gagal mengimport anotasi.", + "No annotations could be located in this book.": "Tiada anotasi dapat dikesan dalam buku ini.", + "Imported {{count}} annotations_other": "{{count}} anotasi diimport", + "No new annotations to import": "Tiada anotasi baharu untuk diimport", + "Import from Moon+ Reader": "Import dari Moon+ Reader" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 4ef95f32..3e1ee79e 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Weet u zeker dat u alle {{count}} markeringen en notities wilt wissen?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Laat leeg om de officiële Readwise-API te gebruiken. Stel alleen een aangepaste URL in om een zelf-gehoste, Readwise-compatibele service te benaderen.", - "Custom URL": "Aangepaste URL" + "Custom URL": "Aangepaste URL", + "Book is not ready yet, please try again.": "Het boek is nog niet gereed, probeer het opnieuw.", + "Select Moon+ Reader Export File": "Moon+ Reader-exportbestand selecteren", + "Failed to read the selected file.": "Kan het geselecteerde bestand niet lezen.", + "No annotations found in the file.": "Geen annotaties gevonden in het bestand.", + "Failed to import annotations.": "Kan annotaties niet importeren.", + "No annotations could be located in this book.": "Kan geen annotaties in dit boek vinden.", + "Imported {{count}} annotations_one": "{{count}} annotatie geïmporteerd", + "Imported {{count}} annotations_other": "{{count}} annotaties geïmporteerd", + "No new annotations to import": "Geen nieuwe annotaties om te importeren", + "Import from Moon+ Reader": "Importeren uit Moon+ Reader" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 43017364..6d56497a 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1494,5 +1494,17 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Czy na pewno chcesz wyczyścić wszystkie {{count}} zaznaczeń i notatek?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Pozostaw puste, aby używać oficjalnego API Readwise. Ustaw niestandardowy adres URL tylko po to, aby kierować ruch do samodzielnie hostowanej usługi zgodnej z Readwise.", - "Custom URL": "Niestandardowy adres URL" + "Custom URL": "Niestandardowy adres URL", + "Book is not ready yet, please try again.": "Książka nie jest jeszcze gotowa, spróbuj ponownie.", + "Select Moon+ Reader Export File": "Wybierz plik eksportu Moon+ Reader", + "Failed to read the selected file.": "Nie udało się odczytać wybranego pliku.", + "No annotations found in the file.": "Nie znaleziono adnotacji w pliku.", + "Failed to import annotations.": "Nie udało się zaimportować adnotacji.", + "No annotations could be located in this book.": "Nie udało się zlokalizować żadnych adnotacji w tej książce.", + "Imported {{count}} annotations_one": "Zaimportowano {{count}} adnotację", + "Imported {{count}} annotations_few": "Zaimportowano {{count}} adnotacje", + "Imported {{count}} annotations_many": "Zaimportowano {{count}} adnotacji", + "Imported {{count}} annotations_other": "Zaimportowano {{count}} adnotacji", + "No new annotations to import": "Brak nowych adnotacji do zaimportowania", + "Import from Moon+ Reader": "Importuj z Moon+ Reader" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 769c33e8..f7537dc6 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Tem certeza de que deseja limpar todos os {{count}} destaques e notas?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Deixe em branco para usar a API oficial do Readwise. Defina um URL personalizado apenas para direcionar a um serviço auto-hospedado compatível com o Readwise.", - "Custom URL": "URL personalizado" + "Custom URL": "URL personalizado", + "Book is not ready yet, please try again.": "O livro ainda não está pronto, tente novamente.", + "Select Moon+ Reader Export File": "Selecionar arquivo de exportação do Moon+ Reader", + "Failed to read the selected file.": "Não foi possível ler o arquivo selecionado.", + "No annotations found in the file.": "Nenhuma anotação encontrada no arquivo.", + "Failed to import annotations.": "Não foi possível importar as anotações.", + "No annotations could be located in this book.": "Não foi possível localizar anotações neste livro.", + "Imported {{count}} annotations_one": "{{count}} anotação importada", + "Imported {{count}} annotations_many": "{{count}} anotações importadas", + "Imported {{count}} annotations_other": "{{count}} anotações importadas", + "No new annotations to import": "Não há novas anotações para importar", + "Import from Moon+ Reader": "Importar do Moon+ Reader" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 018aec27..220465db 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Tem certeza de que deseja limpar todos os {{count}} destaques e notas?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Deixe em branco para utilizar a API oficial do Readwise. Defina um URL personalizado apenas para direcionar para um serviço auto-alojado compatível com o Readwise.", - "Custom URL": "URL personalizado" + "Custom URL": "URL personalizado", + "Book is not ready yet, please try again.": "O livro ainda não está pronto, tente novamente.", + "Select Moon+ Reader Export File": "Selecionar ficheiro de exportação do Moon+ Reader", + "Failed to read the selected file.": "Não foi possível ler o ficheiro selecionado.", + "No annotations found in the file.": "Nenhuma anotação encontrada no ficheiro.", + "Failed to import annotations.": "Não foi possível importar as anotações.", + "No annotations could be located in this book.": "Não foi possível localizar anotações neste livro.", + "Imported {{count}} annotations_one": "{{count}} anotação importada", + "Imported {{count}} annotations_many": "{{count}} anotações importadas", + "Imported {{count}} annotations_other": "{{count}} anotações importadas", + "No new annotations to import": "Não há novas anotações para importar", + "Import from Moon+ Reader": "Importar do Moon+ Reader" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 6bc310d8..72bc7957 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1467,5 +1467,16 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Sigur ștergeți toate cele {{count}} de evidențieri și note?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Lăsați necompletat pentru a folosi API-ul oficial Readwise. Setați un URL personalizat doar pentru a viza un serviciu găzduit local, compatibil cu Readwise.", - "Custom URL": "URL personalizat" + "Custom URL": "URL personalizat", + "Book is not ready yet, please try again.": "Cartea nu este încă gata, încearcă din nou.", + "Select Moon+ Reader Export File": "Selectează fișierul de export Moon+ Reader", + "Failed to read the selected file.": "Citirea fișierului selectat a eșuat.", + "No annotations found in the file.": "Nu s-au găsit adnotări în fișier.", + "Failed to import annotations.": "Importul adnotărilor a eșuat.", + "No annotations could be located in this book.": "Nu s-a putut localiza nicio adnotare în această carte.", + "Imported {{count}} annotations_one": "{{count}} adnotare importată", + "Imported {{count}} annotations_few": "{{count}} adnotări importate", + "Imported {{count}} annotations_other": "{{count}} de adnotări importate", + "No new annotations to import": "Nicio adnotare nouă de importat", + "Import from Moon+ Reader": "Importă din Moon+ Reader" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 72c1598f..ad2c9151 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1494,5 +1494,17 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Очистить все {{count}} выделений и заметок?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Оставьте пустым, чтобы использовать официальный API Readwise. Указывайте собственный URL только для подключения к самостоятельно размещённому сервису, совместимому с Readwise.", - "Custom URL": "Пользовательский URL" + "Custom URL": "Пользовательский URL", + "Book is not ready yet, please try again.": "Книга ещё не готова, попробуйте снова.", + "Select Moon+ Reader Export File": "Выберите файл экспорта Moon+ Reader", + "Failed to read the selected file.": "Не удалось прочитать выбранный файл.", + "No annotations found in the file.": "В файле не найдено аннотаций.", + "Failed to import annotations.": "Не удалось импортировать аннотации.", + "No annotations could be located in this book.": "Не удалось найти аннотации в этой книге.", + "Imported {{count}} annotations_one": "Импортирована {{count}} аннотация", + "Imported {{count}} annotations_few": "Импортировано {{count}} аннотации", + "Imported {{count}} annotations_many": "Импортировано {{count}} аннотаций", + "Imported {{count}} annotations_other": "Импортировано {{count}} аннотаций", + "No new annotations to import": "Нет новых аннотаций для импорта", + "Import from Moon+ Reader": "Импорт из Moon+ Reader" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index d2c86b24..17560cb9 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "ඉස්මතු කිරීම් සහ සටහන් {{count}}ම හිස් කිරීමට අවශ්‍ය බව විශ්වාසද?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "නිල Readwise API භාවිතා කිරීමට හිස්ව තබන්න. ස්වයං-සත්කාරක, Readwise-අනුකූල සේවාවක් ඉලක්ක කිරීමට පමණක් අභිරුචි URL එකක් සකසන්න.", - "Custom URL": "අභිරුචි URL" + "Custom URL": "අභිරුචි URL", + "Book is not ready yet, please try again.": "පොත තවම සූදානම් නැත, කරුණාකර නැවත උත්සාහ කරන්න.", + "Select Moon+ Reader Export File": "Moon+ Reader නිර්යාත ගොනුව තෝරන්න", + "Failed to read the selected file.": "තෝරාගත් ගොනුව කියවීමට අසමත් විය.", + "No annotations found in the file.": "ගොනුවේ සටහන් කිසිවක් හමු නොවීය.", + "Failed to import annotations.": "සටහන් ආයාත කිරීමට අසමත් විය.", + "No annotations could be located in this book.": "මෙම පොතේ සටහන් කිසිවක් සොයාගත නොහැකි විය.", + "Imported {{count}} annotations_one": "සටහන් {{count}}ක් ආයාත කරන ලදී", + "Imported {{count}} annotations_other": "සටහන් {{count}}ක් ආයාත කරන ලදී", + "No new annotations to import": "ආයාත කිරීමට නව සටහන් නැත", + "Import from Moon+ Reader": "Moon+ Reader වෙතින් ආයාත කරන්න" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 627f345e..c0d8e8e9 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1494,5 +1494,17 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Ali res želite počistiti vseh {{count}} označb in opomb?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Pustite prazno za uporabo uradnega API-ja Readwise. URL po meri nastavite samo za ciljanje na samostojno gostovano storitev, združljivo z Readwise.", - "Custom URL": "URL po meri" + "Custom URL": "URL po meri", + "Book is not ready yet, please try again.": "Knjiga še ni pripravljena, poskusite znova.", + "Select Moon+ Reader Export File": "Izberite izvozno datoteko Moon+ Reader", + "Failed to read the selected file.": "Izbrane datoteke ni mogoče prebrati.", + "No annotations found in the file.": "V datoteki ni najdenih opomb.", + "Failed to import annotations.": "Opomb ni mogoče uvoziti.", + "No annotations could be located in this book.": "V tej knjigi ni mogoče najti nobenih opomb.", + "Imported {{count}} annotations_one": "Uvožena {{count}} opomba", + "Imported {{count}} annotations_two": "Uvoženi {{count}} opombi", + "Imported {{count}} annotations_few": "Uvožene {{count}} opombe", + "Imported {{count}} annotations_other": "Uvoženih {{count}} opomb", + "No new annotations to import": "Ni novih opomb za uvoz", + "Import from Moon+ Reader": "Uvozi iz Moon+ Reader" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index f24ada83..b423434a 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Är du säker på att du vill rensa alla {{count}} markeringar och anteckningar?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Lämna tomt för att använda det officiella Readwise-API:et. Ange en anpassad URL endast för att rikta mot en självhostad, Readwise-kompatibel tjänst.", - "Custom URL": "Anpassad URL" + "Custom URL": "Anpassad URL", + "Book is not ready yet, please try again.": "Boken är inte redo än, försök igen.", + "Select Moon+ Reader Export File": "Välj exportfil från Moon+ Reader", + "Failed to read the selected file.": "Kunde inte läsa den valda filen.", + "No annotations found in the file.": "Inga anteckningar hittades i filen.", + "Failed to import annotations.": "Kunde inte importera anteckningar.", + "No annotations could be located in this book.": "Kunde inte hitta några anteckningar i den här boken.", + "Imported {{count}} annotations_one": "{{count}} anteckning importerad", + "Imported {{count}} annotations_other": "{{count}} anteckningar importerade", + "No new annotations to import": "Inga nya anteckningar att importera", + "Import from Moon+ Reader": "Importera från Moon+ Reader" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index bb2c32ab..5b242554 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "அனைத்து {{count}} சிறப்பம்சங்கள் மற்றும் குறிப்புகளையும் அழிக்க வேண்டும் என்பது உறுதியா?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "அதிகாரப்பூர்வ Readwise API ஐப் பயன்படுத்த காலியாக விடவும். சுய-ஹோஸ்ட் செய்யப்பட்ட, Readwise-இணக்கமான சேவையை இலக்காகக் கொள்ள மட்டுமே தனிப்பயன் URL ஐ அமைக்கவும்.", - "Custom URL": "தனிப்பயன் URL" + "Custom URL": "தனிப்பயன் URL", + "Book is not ready yet, please try again.": "புத்தகம் இன்னும் தயாராக இல்லை, மீண்டும் முயற்சிக்கவும்.", + "Select Moon+ Reader Export File": "Moon+ Reader ஏற்றுமதி கோப்பைத் தேர்ந்தெடுக்கவும்", + "Failed to read the selected file.": "தேர்ந்தெடுத்த கோப்பைப் படிக்க முடியவில்லை.", + "No annotations found in the file.": "கோப்பில் எந்தக் குறிப்புகளும் இல்லை.", + "Failed to import annotations.": "குறிப்புகளை இறக்குமதி செய்ய முடியவில்லை.", + "No annotations could be located in this book.": "இந்தப் புத்தகத்தில் எந்தக் குறிப்புகளையும் கண்டறிய முடியவில்லை.", + "Imported {{count}} annotations_one": "{{count}} குறிப்பு இறக்குமதி செய்யப்பட்டது", + "Imported {{count}} annotations_other": "{{count}} குறிப்புகள் இறக்குமதி செய்யப்பட்டன", + "No new annotations to import": "இறக்குமதி செய்ய புதிய குறிப்புகள் இல்லை", + "Import from Moon+ Reader": "Moon+ Reader இலிருந்து இறக்குமதி செய்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 818058cf..3116e4b7 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "แน่ใจหรือไม่ว่าต้องการล้างไฮไลต์และบันทึกทั้งหมด {{count}} รายการ?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "เว้นว่างไว้เพื่อใช้ API อย่างเป็นทางการของ Readwise ตั้งค่า URL ที่กำหนดเองเฉพาะเมื่อต้องการเชื่อมต่อกับบริการที่โฮสต์เองซึ่งเข้ากันได้กับ Readwise", - "Custom URL": "URL ที่กำหนดเอง" + "Custom URL": "URL ที่กำหนดเอง", + "Book is not ready yet, please try again.": "หนังสือยังไม่พร้อม โปรดลองอีกครั้ง", + "Select Moon+ Reader Export File": "เลือกไฟล์ส่งออกของ Moon+ Reader", + "Failed to read the selected file.": "อ่านไฟล์ที่เลือกไม่สำเร็จ", + "No annotations found in the file.": "ไม่พบคำอธิบายประกอบในไฟล์", + "Failed to import annotations.": "นำเข้าคำอธิบายประกอบไม่สำเร็จ", + "No annotations could be located in this book.": "ไม่พบตำแหน่งของคำอธิบายประกอบในหนังสือเล่มนี้", + "Imported {{count}} annotations_other": "นำเข้าคำอธิบายประกอบ {{count}} รายการแล้ว", + "No new annotations to import": "ไม่มีคำอธิบายประกอบใหม่ให้นำเข้า", + "Import from Moon+ Reader": "นำเข้าจาก Moon+ Reader" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index de84e721..14fa2d59 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Tüm {{count}} vurgu ve notu temizlemek istediğinizden emin misiniz?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Resmi Readwise API'sini kullanmak için boş bırakın. Özel bir URL'yi yalnızca kendi barındırdığınız, Readwise uyumlu bir hizmeti hedeflemek için ayarlayın.", - "Custom URL": "Özel URL" + "Custom URL": "Özel URL", + "Book is not ready yet, please try again.": "Kitap henüz hazır değil, lütfen tekrar deneyin.", + "Select Moon+ Reader Export File": "Moon+ Reader dışa aktarma dosyasını seçin", + "Failed to read the selected file.": "Seçilen dosya okunamadı.", + "No annotations found in the file.": "Dosyada açıklama bulunamadı.", + "Failed to import annotations.": "Açıklamalar içe aktarılamadı.", + "No annotations could be located in this book.": "Bu kitapta hiçbir açıklama konumlandırılamadı.", + "Imported {{count}} annotations_one": "{{count}} açıklama içe aktarıldı", + "Imported {{count}} annotations_other": "{{count}} açıklama içe aktarıldı", + "No new annotations to import": "İçe aktarılacak yeni açıklama yok", + "Import from Moon+ Reader": "Moon+ Reader'dan içe aktar" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index a8ffb321..7c34b2a4 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1494,5 +1494,17 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Очистити всі {{count}} виділень та нотаток?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Залиште порожнім, щоб використовувати офіційний API Readwise. Указуйте власну URL-адресу лише для підключення до самостійно розміщеної служби, сумісної з Readwise.", - "Custom URL": "Власний URL" + "Custom URL": "Власний URL", + "Book is not ready yet, please try again.": "Книга ще не готова, спробуйте ще раз.", + "Select Moon+ Reader Export File": "Виберіть файл експорту Moon+ Reader", + "Failed to read the selected file.": "Не вдалося прочитати вибраний файл.", + "No annotations found in the file.": "У файлі не знайдено анотацій.", + "Failed to import annotations.": "Не вдалося імпортувати анотації.", + "No annotations could be located in this book.": "Не вдалося знайти анотації в цій книзі.", + "Imported {{count}} annotations_one": "Імпортовано {{count}} анотацію", + "Imported {{count}} annotations_few": "Імпортовано {{count}} анотації", + "Imported {{count}} annotations_many": "Імпортовано {{count}} анотацій", + "Imported {{count}} annotations_other": "Імпортовано {{count}} анотацій", + "No new annotations to import": "Немає нових анотацій для імпорту", + "Import from Moon+ Reader": "Імпорт із Moon+ Reader" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index 29ffe883..d02dfc5b 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -1440,5 +1440,15 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Barcha {{count}} ta belgilash va eslatmani tozalamoqchimisiz?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Rasmiy Readwise API'sidan foydalanish uchun bo'sh qoldiring. Maxsus URL manzilini faqat o'zingiz joylashtirgan, Readwise bilan mos xizmatga ulanish uchun belgilang.", - "Custom URL": "Maxsus URL" + "Custom URL": "Maxsus URL", + "Book is not ready yet, please try again.": "Kitob hali tayyor emas, qaytadan urinib koʻring.", + "Select Moon+ Reader Export File": "Moon+ Reader eksport faylini tanlang", + "Failed to read the selected file.": "Tanlangan faylni oʻqib boʻlmadi.", + "No annotations found in the file.": "Faylda hech qanday izoh topilmadi.", + "Failed to import annotations.": "Izohlarni import qilib boʻlmadi.", + "No annotations could be located in this book.": "Bu kitobda hech qanday izohni aniqlab boʻlmadi.", + "Imported {{count}} annotations_one": "{{count}} ta izoh import qilindi", + "Imported {{count}} annotations_other": "{{count}} ta izoh import qilindi", + "No new annotations to import": "Import qilish uchun yangi izohlar yoʻq", + "Import from Moon+ Reader": "Moon+ Reader’dan import qilish" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index a71b2b87..d1256fcb 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "Bạn có chắc muốn xóa tất cả {{count}} đánh dấu và ghi chú không?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "Để trống để dùng API Readwise chính thức. Chỉ đặt URL tùy chỉnh để trỏ đến dịch vụ tự lưu trữ tương thích với Readwise.", - "Custom URL": "URL tùy chỉnh" + "Custom URL": "URL tùy chỉnh", + "Book is not ready yet, please try again.": "Sách chưa sẵn sàng, vui lòng thử lại.", + "Select Moon+ Reader Export File": "Chọn tệp xuất của Moon+ Reader", + "Failed to read the selected file.": "Không thể đọc tệp đã chọn.", + "No annotations found in the file.": "Không tìm thấy chú thích nào trong tệp.", + "Failed to import annotations.": "Không thể nhập chú thích.", + "No annotations could be located in this book.": "Không thể xác định vị trí chú thích nào trong cuốn sách này.", + "Imported {{count}} annotations_other": "Đã nhập {{count}} chú thích", + "No new annotations to import": "Không có chú thích mới để nhập", + "Import from Moon+ Reader": "Nhập từ Moon+ Reader" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index d1eb4cc5..130db9a8 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "确定要清除全部 {{count}} 个高亮和笔记吗?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "留空以使用官方 Readwise API。仅在需要连接自托管的 Readwise 兼容服务时才设置自定义 URL。", - "Custom URL": "自定义 URL" + "Custom URL": "自定义 URL", + "Book is not ready yet, please try again.": "书籍尚未就绪,请重试。", + "Select Moon+ Reader Export File": "选择 Moon+ Reader 导出文件", + "Failed to read the selected file.": "无法读取所选文件。", + "No annotations found in the file.": "文件中未找到注释。", + "Failed to import annotations.": "导入注释失败。", + "No annotations could be located in this book.": "无法在本书中定位任何注释。", + "Imported {{count}} annotations_other": "已导入 {{count}} 条注释", + "No new annotations to import": "没有可导入的新注释", + "Import from Moon+ Reader": "从 Moon+ Reader 导入" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index af8fe418..192be8f3 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1413,5 +1413,14 @@ "Are you sure to clear all {{count}} highlights and notes?_other": "確定要清除全部 {{count}} 個標記和筆記嗎?", "Discord": "Discord", "Leave blank to use the official Readwise API. Set a custom URL only to target a self-hosted, Readwise-compatible service.": "留空以使用官方 Readwise API。僅在需要連接自架的 Readwise 相容服務時才設定自訂 URL。", - "Custom URL": "自訂 URL" + "Custom URL": "自訂 URL", + "Book is not ready yet, please try again.": "書籍尚未就緒,請重試。", + "Select Moon+ Reader Export File": "選擇 Moon+ Reader 匯出檔案", + "Failed to read the selected file.": "無法讀取所選檔案。", + "No annotations found in the file.": "檔案中未找到註解。", + "Failed to import annotations.": "匯入註解失敗。", + "No annotations could be located in this book.": "無法在本書中定位任何註解。", + "Imported {{count}} annotations_other": "已匯入 {{count}} 條註解", + "No new annotations to import": "沒有可匯入的新註解", + "Import from Moon+ Reader": "從 Moon+ Reader 匯入" } diff --git a/apps/readest-app/src/__tests__/services/mrexpt-import.test.ts b/apps/readest-app/src/__tests__/services/mrexpt-import.test.ts new file mode 100644 index 00000000..b2df0662 --- /dev/null +++ b/apps/readest-app/src/__tests__/services/mrexpt-import.test.ts @@ -0,0 +1,67 @@ +import { describe, test, expect } from 'vitest'; +import type { BookNote } from '@/types/book'; +import { mergeImportedBookNotes } from '@/services/annotation/providers/mrexpt'; + +const makeNote = (overrides: Partial = {}): BookNote => ({ + id: 'mrexpt-1', + type: 'annotation', + cfi: 'epubcfi(/6/4!/4)', + text: 'hello', + style: 'highlight', + color: 'yellow', + note: '', + createdAt: 1000, + updatedAt: 1000, + ...overrides, +}); + +describe('mergeImportedBookNotes', () => { + test('adds notes that do not yet exist', () => { + const result = mergeImportedBookNotes([], [makeNote()]); + expect(result.added).toBe(1); + expect(result.updated).toBe(0); + expect(result.applied).toHaveLength(1); + expect(result.merged).toHaveLength(1); + }); + + test('skips notes that were already imported unchanged', () => { + const existing = [makeNote()]; + const result = mergeImportedBookNotes(existing, [makeNote()]); + expect(result.added).toBe(0); + expect(result.updated).toBe(0); + expect(result.applied).toHaveLength(0); + expect(result.merged).toHaveLength(1); + }); + + test('reports zero changes when every incoming note is a duplicate', () => { + // Regression: a re-import of an unchanged file must surface as + // "nothing new" (added + updated === 0), never as "Imported 0". + const existing = [makeNote({ id: 'a' }), makeNote({ id: 'b' })]; + const incoming = [makeNote({ id: 'a' }), makeNote({ id: 'b' })]; + const result = mergeImportedBookNotes(existing, incoming); + expect(result.added + result.updated).toBe(0); + }); + + test('resurrects a soft-deleted note', () => { + const existing = [makeNote({ deletedAt: 5000 })]; + const result = mergeImportedBookNotes(existing, [makeNote()]); + expect(result.added).toBe(1); + expect(result.merged[0]!.deletedAt).toBeNull(); + expect(result.applied).toHaveLength(1); + }); + + test('updates an existing note when the incoming copy is newer', () => { + const existing = [makeNote({ note: 'old', updatedAt: 1000 })]; + const result = mergeImportedBookNotes(existing, [makeNote({ note: 'new', updatedAt: 2000 })]); + expect(result.updated).toBe(1); + expect(result.added).toBe(0); + expect(result.merged[0]!.note).toBe('new'); + }); + + test('preserves unrelated existing notes', () => { + const existing = [makeNote({ id: 'keep', type: 'bookmark' })]; + const result = mergeImportedBookNotes(existing, [makeNote({ id: 'new' })]); + expect(result.merged).toHaveLength(2); + expect(result.merged.some((n) => n.id === 'keep')).toBe(true); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index bc30e543..cef0cbfd 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -52,6 +52,12 @@ import { setProofreadRulesVisibility } from '@/app/reader/components/ProofreadRu import ExportMarkdownDialog from './ExportMarkdownDialog'; import Alert from '@/components/Alert'; import ModalPortal from '@/components/ModalPortal'; +import { useFileSelector } from '@/hooks/useFileSelector'; +import { parseMrexpt } from '@/utils/mrexpt'; +import { + convertMrexptEntriesToBookNotes, + mergeImportedBookNotes, +} from '@/services/annotation/providers/mrexpt'; const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const _ = useTranslation(); @@ -65,6 +71,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const { clearBooknotesNav } = useSidebarStore(); const { listenToNativeTouchEvents } = useDeviceControlStore(); const { loadCustomDictionaries } = useCustomDictionaryStore(); + const { selectFiles } = useFileSelector(appService, _); useNotesSync(bookKey); useReadwiseSync(bookKey); @@ -484,9 +491,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { useEffect(() => { eventDispatcher.on('export-annotations', handleExportMarkdown); eventDispatcher.on('clear-annotations', handleClearAnnotations); + eventDispatcher.on('import-mrexpt', handleImportMrexpt); return () => { eventDispatcher.off('export-annotations', handleExportMarkdown); eventDispatcher.off('clear-annotations', handleClearAnnotations); + eventDispatcher.off('import-mrexpt', handleImportMrexpt); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -859,6 +868,126 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { [selection?.text], ); + const handleImportMrexpt = async (event: CustomEvent) => { + const { bookKey: importBookKey } = event.detail; + if (bookKey !== importBookKey) return; + + const { bookDoc } = bookData; + if (!bookDoc) { + eventDispatcher.dispatch('toast', { + type: 'warning', + message: _('Book is not ready yet, please try again.'), + timeout: 2000, + }); + return; + } + + // Pick the .mrexpt file. + const result = await selectFiles({ + type: 'generic', + accept: '.mrexpt,text/plain', + extensions: ['mrexpt', 'txt'], + multiple: false, + dialogTitle: _('Select Moon+ Reader Export File'), + }); + if (result.error || result.files.length === 0) return; + const selectedFile = result.files[0]!; + + // Read the file content as text on both Web (File) and Tauri (path). + let content = ''; + try { + if (selectedFile.file) { + content = await selectedFile.file.text(); + } else if (selectedFile.path) { + content = (await appService?.readFile(selectedFile.path, 'None', 'text')) as string; + } + } catch (e) { + console.warn('Failed to read mrexpt file:', e); + } + + if (!content) { + eventDispatcher.dispatch('toast', { + type: 'warning', + message: _('Failed to read the selected file.'), + timeout: 2000, + }); + return; + } + + const entries = parseMrexpt(content); + if (entries.length === 0) { + eventDispatcher.dispatch('toast', { + type: 'info', + message: _('No annotations found in the file.'), + timeout: 2000, + }); + return; + } + + let conversion; + try { + conversion = await convertMrexptEntriesToBookNotes(entries, bookDoc, { + highlightStyle: settings.globalReadSettings.highlightStyle, + highlightColor: + settings.globalReadSettings.highlightStyles[settings.globalReadSettings.highlightStyle], + }); + } catch (e) { + console.warn('Failed to convert mrexpt entries:', e); + eventDispatcher.dispatch('toast', { + type: 'warning', + message: _('Failed to import annotations.'), + timeout: 3000, + }); + return; + } + + if (conversion.notes.length === 0) { + eventDispatcher.dispatch('toast', { + type: 'info', + message: _('No annotations could be located in this book.'), + timeout: 2500, + }); + return; + } + + // Merge into the current book config, deduplicating by note id and + // preferring the latest updatedAt for any conflicting entries. + const config = getConfig(bookKey)!; + const { merged, applied, added, updated } = mergeImportedBookNotes( + config.booknotes ?? [], + conversion.notes, + ); + const updatedConfig = updateBooknotes(bookKey, merged); + if (updatedConfig) { + saveConfig(envConfig, bookKey, updatedConfig, settings); + } + + // Apply imported (or resurrected) annotations to all live views so + // they appear immediately. We only re-draw the notes that actually + // changed in this round, otherwise duplicate addAnnotation calls + // can confuse the overlay layer. + const views = getViewsById(bookKey.split('-')[0]!); + for (const note of applied) { + try { + views.forEach((v) => v?.addAnnotation(note)); + } catch (err) { + console.warn('Failed to add imported annotation', { note, err }); + } + } + + // A single result toast: the count if anything changed, otherwise a + // plain "nothing new" hint for a repeated import of the same file. + const imported = added + updated; + eventDispatcher.dispatch('toast', { + type: 'info', + message: + imported > 0 + ? _('Imported {{count}} annotations', { count: imported }) + : _('No new annotations to import'), + timeout: 2500, + }); + }; + const handleExportMarkdown = async (event: CustomEvent) => { const { bookKey: exportBookKey } = event.detail; if (bookKey !== exportBookKey) return; diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx index 11851282..9a5de248 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx @@ -71,6 +71,10 @@ const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) eventDispatcher.dispatch('export-annotations', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; + const handleImportFromMoonReader = () => { + eventDispatcher.dispatch('import-mrexpt', { bookKey: sideBarBookKey }); + setIsDropdownOpen?.(false); + }; const handleToggleSortTOC = () => { setIsSortedTOC((prev) => !prev); setIsDropdownOpen?.(false); @@ -195,6 +199,7 @@ const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) + { + const byId = new Map(); + for (const note of existing) byId.set(note.id, note); + + const applied: BookNote[] = []; + let added = 0; + let updated = 0; + + for (const note of incoming) { + const prev = byId.get(note.id); + if (!prev) { + byId.set(note.id, note); + added += 1; + applied.push(note); + } else if (prev.deletedAt) { + const resurrected: BookNote = { + ...prev, + ...note, + deletedAt: null, + updatedAt: Date.now(), + }; + byId.set(note.id, resurrected); + added += 1; + applied.push(resurrected); + } else if (prev.updatedAt < note.updatedAt) { + const merged: BookNote = { ...prev, ...note }; + byId.set(note.id, merged); + updated += 1; + applied.push(merged); + } + } + + return { merged: Array.from(byId.values()), applied, added, updated }; +}; + +export interface MrexptConversionOptions { + /** + * Color used for plain highlights (Moon+ Reader's "highlight" color + * doesn't map cleanly to Readest, so we pick a default). + */ + highlightColor?: HighlightColor; + /** Highlight style applied to all generated notes. */ + highlightStyle?: HighlightStyle; + /** + * Optional CFI factory. When provided, will be used in preference to + * the built-in best-effort CFI generation. The factory receives the + * spine index, section, document, and matched range. + */ + cfiFactory?: ( + spineIndex: number, + section: SectionItem, + doc: Document, + range: Range, + ) => string | undefined; +} + +/** + * Build a fallback CFI string for a given spine index. Returns the section's + * own CFI (form: `epubcfi(/6/4!)`) which foliate-js can resolve to the start + * of the chapter. We deliberately don't try to invent an in-document path + * like `/4/2/1:0` because that path is rarely valid for a real book and + * would make `view.goTo()` fail to navigate. + */ +const buildFallbackSectionCfi = (spineIndex: number, section: SectionItem | undefined): string => { + if (section?.cfi) return section.cfi; + const step = 2 * (spineIndex + 1); + return `epubcfi(/6/${step}!)`; +}; + +/** + * Build a real, navigable CFI by combining the section's spine-step CFI + * with the in-section path produced from the matched Range. This mirrors + * what foliate-js's view.getCFI(index, range) does internally: + * CFI.joinIndir(sectionCfi, CFI.fromRange(range)) + * If anything goes wrong we fall back to the chapter-start CFI so the + * note still lands on the right chapter, just not the exact word. + */ +const buildRangeCfi = ( + spineIndex: number, + section: SectionItem, + doc: Document, + range: Range, + factory?: MrexptConversionOptions['cfiFactory'], +): string => { + const factoryResult = factory?.(spineIndex, section, doc, range); + if (factoryResult) return factoryResult; + if (section.cfi) { + try { + const rangeCfi = CFI.fromRange(range); + if (rangeCfi) return CFI.joinIndir(section.cfi, rangeCfi); + } catch (e) { + console.warn('Failed to build range CFI for mrexpt entry:', e); + } + } + return buildFallbackSectionCfi(spineIndex, section); +}; + +/** + * Resolve a TOC href to its 0-based spine index by looking up the + * corresponding SectionItem. + * + * BookDoc.splitTOCHref returns [sectionId, fragmentId?] where sectionId is + * the path-resolved manifest item href (matching SectionItem.id). + */ +const buildHrefToSpineIndex = (bookDoc: BookDoc): Map => { + const map = new Map(); + const sections = bookDoc.sections ?? []; + sections.forEach((section, idx) => { + if (section.id) map.set(section.id, idx); + if (section.href) map.set(section.href, idx); + }); + return map; +}; + +/** + * Build a mapping from NCX navPoint 0-based index to spine index using + * BookDoc.toc. The mrexpt format encodes the chapter as the navPoint index + * (field b4), which corresponds to the in-document order of TOC items. + */ +const buildNavpointToSpine = (bookDoc: BookDoc): number[] => { + const result: number[] = []; + const hrefToSpine = buildHrefToSpineIndex(bookDoc); + const flat = collectAllTocItems(bookDoc.toc ?? []); + for (const item of flat) { + if (!item.href) { + result.push(-1); + continue; + } + const [sectionId] = bookDoc.splitTOCHref(item.href) as [string | undefined]; + let spineIdx = -1; + if (sectionId !== undefined) { + spineIdx = hrefToSpine.get(sectionId) ?? -1; + } + if (spineIdx < 0) { + // Try a basename match (some books store relative paths in TOC). + const candidate = sectionId ?? item.href; + const basename = candidate.split('/').pop() ?? candidate; + const fallback = (bookDoc.sections ?? []).findIndex((s) => { + const sid = s.id || s.href || ''; + return sid.endsWith(`/${basename}`) || sid === basename; + }); + spineIdx = fallback; + } + result.push(spineIdx); + } + return result; +}; + +/** + * Locate the first occurrence of `word` in a Document and return a Range + * covering it. The match is case-insensitive; for ASCII words we keep a + * trailing word boundary (so `cat` doesn't bleed into `category`) and a + * tolerant leading boundary that also accepts a common English prefix — + * this way Moon+ Reader entries like `happy` still locate words such as + * `unhappy` or `dishappy` in the body text. + * + * For non-ASCII text (e.g. CJK) we fall back to a plain case-insensitive + * substring search since regex word boundaries don't apply. + */ +const findWordRange = (doc: Document, word: string): Range | null => { + if (!word) return null; + const body = doc.body; + if (!body) return null; + + const isAsciiWord = /^[A-Za-z][A-Za-z'-]*$/.test(word); + const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Common English prefixes that frequently attach to standalone words. + // Order matters when patterns overlap (longest first) so the regex + // engine prefers the longer, more specific prefix when both could + // match (e.g. "under" before "un"). + const prefixGroup = + '(?:under|over|trans|inter|super|extra|counter|semi|anti|pre|mis|non|dis|sub|out|up|re|un|in|im|il|ir|en|em)'; + // Common English suffixes (kept from the original implementation, plus a + // few comparative/adverbial forms typical for adjectives). + const suffixGroup = '(?:ly|y|ing|ed|d|es|s|er|est|ness|ment|ful|less|able|ible)'; + const pattern = isAsciiWord + ? // Left side: either a real word boundary OR a known prefix that itself + // sits at a word boundary. Right side: still requires a word boundary, + // so we won't match the target inside an unrelated longer word. + new RegExp(`(?:\\b|\\b${prefixGroup})${escaped}${suffixGroup}?\\b`, 'i') + : new RegExp(escaped, 'i'); + + const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT); + let node = walker.nextNode() as Text | null; + while (node) { + const text = node.data; + if (text) { + const match = pattern.exec(text); + if (match) { + // Locate the actual start of `word` inside the match (the regex + // also captures any leading prefix, so match.index points to the + // prefix start). Highlight the original word, not the prefix. + const wordStart = match[0].toLowerCase().lastIndexOf(word.toLowerCase()); + const start = wordStart >= 0 ? match.index + wordStart : match.index; + const end = wordStart >= 0 ? start + word.length : match.index + match[0].length; + const range = doc.createRange(); + range.setStart(node, start); + range.setEnd(node, end); + return range; + } + } + node = walker.nextNode() as Text | null; + } + return null; +}; + +const dedupeEntries = (entries: MrexptEntry[]): MrexptEntry[] => { + const seen = new Set(); + const out: MrexptEntry[] = []; + for (const entry of entries) { + const key = `${entry.word.toLowerCase()}|${entry.note}|${entry.b4}|${entry.b6}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(entry); + } + return out; +}; + +const stableNoteId = (entry: MrexptEntry): string => { + // Prefer a stable id derived from the entry so re-imports merge correctly. + if (entry.entryId) return `mrexpt-${entry.entryId}`; + return `mrexpt-${uniqueId()}`; +}; + +/** + * Convert mrexpt entries to BookNote objects, locating each highlight inside + * the BookDoc to obtain a real EPUB CFI. When the exact word cannot be found + * we still emit a note pointing at the start of the chapter so the user + * sees the import in the right place. + */ +export const convertMrexptEntriesToBookNotes = async ( + entries: MrexptEntry[], + bookDoc: BookDoc, + options: MrexptConversionOptions = {}, +): Promise => { + const sections = bookDoc.sections ?? []; + const navpointToSpine = buildNavpointToSpine(bookDoc); + const highlightStyle = options.highlightStyle ?? 'highlight'; + const highlightColor = options.highlightColor ?? 'yellow'; + + const unique = dedupeEntries(entries); + const notes: BookNote[] = []; + let unmatched = 0; + const now = Date.now(); + + // Cache section documents so we don't reparse them per word. + const docCache = new Map(); + const loadSectionDoc = async (idx: number): Promise => { + if (docCache.has(idx)) return docCache.get(idx) ?? null; + const section = sections[idx]; + if (!section?.createDocument) { + docCache.set(idx, null); + return null; + } + try { + const doc = await section.createDocument(); + docCache.set(idx, doc); + return doc; + } catch { + docCache.set(idx, null); + return null; + } + }; + + for (let i = 0; i < unique.length; i++) { + const entry = unique[i]!; + const createdAt = entry.timestamp || now + i; + + let resolvedSpineIdx = -1; + let resolvedRange: Range | null = null; + let resolvedDoc: Document | null = null; + + // Strategy 1: use b4 (navPoint index) -> spine index mapping. + if (entry.b4 >= 0 && entry.b4 < navpointToSpine.length) { + const spineIdx = navpointToSpine[entry.b4]!; + if (spineIdx >= 0 && spineIdx < sections.length) { + const doc = await loadSectionDoc(spineIdx); + if (doc) { + const range = findWordRange(doc, entry.word); + if (range) { + resolvedSpineIdx = spineIdx; + resolvedRange = range; + resolvedDoc = doc; + } else { + // Section located but word not found — still anchor at chapter. + resolvedSpineIdx = spineIdx; + } + } + } + } + + // Strategy 2: scan all sections in order if we still don't have a hit. + if (!resolvedRange) { + for (let s = 0; s < sections.length; s++) { + if (s === resolvedSpineIdx) continue; + const doc = await loadSectionDoc(s); + if (!doc) continue; + const range = findWordRange(doc, entry.word); + if (range) { + resolvedSpineIdx = s; + resolvedRange = range; + resolvedDoc = doc; + break; + } + } + } + + if (resolvedSpineIdx < 0) { + unmatched += 1; + continue; + } + + const section = sections[resolvedSpineIdx]!; + const cfi = + resolvedRange && resolvedDoc + ? buildRangeCfi(resolvedSpineIdx, section, resolvedDoc, resolvedRange, options.cfiFactory) + : buildFallbackSectionCfi(resolvedSpineIdx, section); + + if (!resolvedRange) unmatched += 1; + + const note: BookNote = { + id: stableNoteId(entry), + type: 'annotation', + cfi, + text: entry.word, + style: highlightStyle, + color: highlightColor, + note: entry.note, + createdAt, + updatedAt: createdAt, + }; + notes.push(note); + } + + return { notes, unmatched, total: unique.length }; +}; diff --git a/apps/readest-app/src/utils/mrexpt.ts b/apps/readest-app/src/utils/mrexpt.ts new file mode 100644 index 00000000..72f46bbf --- /dev/null +++ b/apps/readest-app/src/utils/mrexpt.ts @@ -0,0 +1,162 @@ +/** + * Moon+ Reader (.mrexpt) export file parser. + * + * The .mrexpt format is a plaintext file produced by the Android app + * "Moon+ Reader". Each entry encodes a highlight or annotation captured by + * the user while reading. The same logic was originally implemented in + * Python (see MultilingualWord/extract_words_from_epub.py) and is ported + * here so Readest can import the entries as BookNotes. + * + * File layout: + * line 0: numeric file header + * line 1: "indent:true|false" + * line 2: "trim:true|false" + * then a sequence of entries separated by a single line containing "#": + * line 0: entry id + * line 1: book title + * line 2: original-case file path + * line 3: lowercase file path + * line 4: b4 — NCX navPoint 0-based index (chapter locator) + * line 5: b5 — paragraph offset high bits (usually 0) + * line 6: b6 — character offset within the document + * line 7: word character length + * line 8: color/marker value (-28160 = pure highlight, others = note color) + * line 9: timestamp (ms) + * line 10: empty + * line 11: empty (pure highlight) or note text (with note) + * line 12: the highlighted word/phrase + * line 13: type marker — "1" pure highlight, "0" with note + * line 14-15: padding zeroes + */ + +export interface MrexptEntry { + /** Highlighted text. */ + word: string; + /** Optional user note attached to the highlight. */ + note: string; + /** NCX navPoint 0-based index (chapter locator). */ + b4: number; + /** Paragraph offset high bits, usually 0. */ + b5: number; + /** Character offset inside the document. */ + b6: number; + /** Word character length as recorded by Moon+ Reader. */ + wordLength: number; + /** Creation timestamp (ms). */ + timestamp: number; + /** Book title as embedded in the entry. */ + bookTitle: string; + /** File path as embedded in the entry. */ + bookPath: string; + /** Original entry id from the file. */ + entryId: string; + /** Whether the entry has an attached note (type marker "0"). */ + hasNote: boolean; +} + +const safeParseInt = (value: string | undefined): number => { + if (!value) return 0; + const trimmed = value.trim(); + if (!trimmed) return 0; + // allow leading minus + const numeric = trimmed.replace(/^-/, ''); + if (!/^\d+$/.test(numeric)) return 0; + const parsed = parseInt(trimmed, 10); + return Number.isFinite(parsed) ? parsed : 0; +}; + +const isNumericLine = (value: string): boolean => { + const trimmed = value.trim(); + if (!trimmed) return false; + return /^-?\d+$/.test(trimmed); +}; + +/** Parse the raw text content of a .mrexpt file into a list of entries. */ +export const parseMrexpt = (content: string): MrexptEntry[] => { + // Normalize line endings, then split on a line containing exactly "#". + // We deliberately use "\n#\n" to mirror the Python implementation. + const normalized = content.replace(/\r\n?/g, '\n'); + const rawEntries = normalized.split('\n#\n'); + + const entries: MrexptEntry[] = []; + + for (let i = 0; i < rawEntries.length; i++) { + let lines = rawEntries[i]!.split('\n'); + + if (i === 0) { + // First block contains the 3-line file header before the first entry. + if (lines.length >= 3 && lines[1]!.startsWith('indent:')) { + lines = lines.slice(3); + } else { + continue; + } + } + + if (lines.length < 13) continue; + + const entryId = (lines[0] ?? '').trim(); + const bookTitle = (lines[1] ?? '').trim(); + const bookPath = (lines[2] ?? '').trim(); + const b4 = safeParseInt(lines[4]); + const b5 = safeParseInt(lines[5]); + const b6 = safeParseInt(lines[6]); + const wordLength = safeParseInt(lines[7]); + const timestamp = safeParseInt(lines[9]); + + let word = ''; + let note = ''; + let hasNote = false; + + if (lines.length >= 14) { + const typeMarker = (lines[13] ?? '').trim(); + if (typeMarker === '0') { + // Annotation: note in line 11, word in line 12. + note = (lines[11] ?? '').trim(); + word = (lines[12] ?? '').trim(); + hasNote = true; + } else if (typeMarker === '1') { + // Pure highlight: line 11 empty, word in line 12. + word = (lines[12] ?? '').trim(); + } else { + // Unknown marker — fall back to the first non-numeric line after 10. + for (let idx = 10; idx < Math.min(lines.length, 15); idx++) { + const candidate = (lines[idx] ?? '').trim(); + if (candidate && !isNumericLine(candidate)) { + word = candidate; + break; + } + } + } + } else { + // Older entries: try line 12 then a non-numeric scan. + word = (lines[12] ?? '').trim(); + if (!word) { + for (let idx = 10; idx < lines.length; idx++) { + const candidate = (lines[idx] ?? '').trim(); + if (candidate && !isNumericLine(candidate)) { + word = candidate; + break; + } + } + } + } + + if (!word) continue; + + entries.push({ + word, + note, + b4, + b5, + b6, + wordLength, + timestamp, + bookTitle, + bookPath, + entryId, + hasNote, + }); + } + + return entries; +};