feat(reader): proofread rule sync, regex, reorder, and dialog refresh (#4700) (#4708)

- Sync library-scope replacement rules across devices (settings whitelist).
  Book and selection rules already ride along the book config.
- Add regex support: a Regex toggle on the selection popup plus a full
  add-rule form (find / replace / scope / regex / case-sensitive) in the
  Proofread Rules manager.
- Reuse Ctrl/Cmd+P to open the rules manager when nothing is selected
  (handleProofread); opens the create-from-selection popup otherwise.
- Translate the whole-word warning, which was hardcoded English.
- Drag-to-reorder rules within each category (dnd-kit), persisted via the
  rule order field across both the book config and global settings.
- Modernize the manager dialog with the settings primitives and a
  btn-contrast CTA; fix mobile height clipping and the inset scrollbar.
- Translate all pending i18n strings across 33 locales (includes the
  delete-confirm strings surfaced by the extractor).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-22 01:01:04 +08:00
committed by GitHub
parent febb0d9a69
commit 9735f497db
42 changed files with 1126 additions and 209 deletions
@@ -464,6 +464,7 @@
"TTS not supported for this document": "تكنولوجيا تحويل النص إلى كلام غير مدعومة لهذا المستند.",
"Reset Password": "إعادة تعيين",
"Show Reading Progress": "إظهار تقدم القراءة",
"Show Progress Bar": "إظهار شريط التقدم",
"Reading Progress Style": "نمط تقدم القراءة",
"Page Number": "رقم الصفحة",
"Percentage": "النسبة المئوية",
@@ -1890,10 +1891,6 @@
"Auto Sync": "مزامنة تلقائية",
"Purged book data: {{title}}": "تم محو بيانات الكتاب: {{title}}",
"Failed to purge book data: {{title}}": "فشل محو بيانات الكتاب: {{title}}",
"Purge Book Data": "محو جميع بيانات الكتاب",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "سيؤدي هذا إلى محو الكتاب وجميع بياناته نهائيًا، بما في ذلك تقدم القراءة والملاحظات والإشارات المرجعية. لا يمكن التراجع عن هذا الإجراء.",
"Purge Data": "محو جميع البيانات",
"Erase the book and all its data, including reading progress": "محو الكتاب وجميع بياناته، بما في ذلك تقدم القراءة",
"More Actions": "إجراءات أخرى",
"Sign in and make the book available to share it": "سجّل الدخول واجعل الكتاب متاحًا لمشاركته",
"Download the book to export it": "نزّل الكتاب لتصديره",
@@ -1912,5 +1909,16 @@
"Failed to save the image": "فشل حفظ الصورة",
"Share Image": "مشاركة الصورة",
"Save Image": "حفظ الصورة",
"Image saved to gallery": "تم حفظ الصورة في المعرض"
"Image saved to gallery": "تم حفظ الصورة في المعرض",
"Please select a whole word or uncheck the \"Whole word\" option.": "يرجى تحديد كلمة كاملة أو إلغاء تحديد خيار \"كلمة كاملة\".",
"Regex:": "Regex:",
"Invalid regular expression": "تعبير نمطي غير صالح",
"Find pattern cannot be empty": "لا يمكن أن يكون نمط البحث فارغًا",
"Add Rule": "إضافة قاعدة",
"Find...": "بحث...",
"Replace with...": "استبدال بـ...",
"Purge & Delete": "مسح وحذف",
"Purge all reading data": "مسح جميع بيانات القراءة",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "يؤدي هذا إلى مسح تقدم القراءة والملاحظات والإشارات المرجعية نهائيًا. لا يمكن التراجع عن هذا الإجراء.",
"Also erase reading progress, notes, and bookmarks.": "يمسح أيضًا تقدم القراءة والملاحظات والإشارات المرجعية."
}
@@ -267,6 +267,7 @@
"Show Remaining Time": "বাকি সময় দেখান",
"Show Remaining Pages": "বাকি পৃষ্ঠা দেখান",
"Show Reading Progress": "পড়ার অগ্রগতি দেখান",
"Show Progress Bar": "অগ্রগতি বার দেখান",
"Reading Progress Style": "পড়ার অগ্রগতির স্টাইল",
"Page Number": "পৃষ্ঠা নম্বর",
"Percentage": "শতাংশ",
@@ -1758,10 +1759,6 @@
"Auto Sync": "স্বয়ংক্রিয় সিঙ্ক",
"Purged book data: {{title}}": "বইয়ের ডেটা মুছে ফেলা হয়েছে: {{title}}",
"Failed to purge book data: {{title}}": "বইয়ের ডেটা মুছতে ব্যর্থ: {{title}}",
"Purge Book Data": "বইয়ের সমস্ত ডেটা মুছুন",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "এটি বইটি এবং পড়ার অগ্রগতি, নোট ও বুকমার্ক সহ এর সমস্ত ডেটা স্থায়ীভাবে মুছে ফেলবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
"Purge Data": "সমস্ত ডেটা মুছুন",
"Erase the book and all its data, including reading progress": "পড়ার অগ্রগতিসহ বই ও এর সমস্ত ডেটা মুছে ফেলুন",
"More Actions": "আরও ক্রিয়া",
"Sign in and make the book available to share it": "শেয়ার করতে সাইন ইন করুন এবং বইটি উপলব্ধ করুন",
"Download the book to export it": "এক্সপোর্ট করতে বইটি ডাউনলোড করুন",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "ছবি সংরক্ষণ করা যায়নি",
"Share Image": "ছবি শেয়ার করুন",
"Save Image": "ছবি সংরক্ষণ করুন",
"Image saved to gallery": "ছবি গ্যালারিতে সংরক্ষিত হয়েছে"
"Image saved to gallery": "ছবি গ্যালারিতে সংরক্ষিত হয়েছে",
"Please select a whole word or uncheck the \"Whole word\" option.": "একটি সম্পূর্ণ শব্দ নির্বাচন করুন অথবা \"সম্পূর্ণ শব্দ\" বিকল্পটি আনচেক করুন।",
"Regex:": "Regex:",
"Invalid regular expression": "অবৈধ রেগুলার এক্সপ্রেশন",
"Find pattern cannot be empty": "অনুসন্ধান প্যাটার্ন খালি রাখা যাবে না",
"Add Rule": "নিয়ম যোগ করুন",
"Find...": "খুঁজুন...",
"Replace with...": "প্রতিস্থাপন করুন...",
"Purge & Delete": "পরিষ্কার করে মুছুন",
"Purge all reading data": "সমস্ত পড়ার ডেটা পরিষ্কার করুন",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "এটি স্থায়ীভাবে পড়ার অগ্রগতি, নোট এবং বুকমার্ক মুছে ফেলে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
"Also erase reading progress, notes, and bookmarks.": "পড়ার অগ্রগতি, নোট এবং বুকমার্কও মুছে ফেলে।"
}
@@ -443,6 +443,7 @@
"TTS not supported for this document": "ཡིག་ཆ་འདིར་ TTS རྒྱབ་སྐྱོར་མི་བྱེད།",
"Reset Password": "གསང་ཨང་བསྐྱར་སྒྲིག",
"Show Reading Progress": "ཀློག་པའི་ཡར་འཕེལ་མངོན་པ།",
"Show Progress Bar": "ཡར་འཕེལ་ཕྲ་རིང་མངོན་པ།",
"Reading Progress Style": "ཀློག་པའི་ཡར་འཕེལ་གྱི་རྣམ་པ།",
"Page Number": "ཤོག་ཨང་།",
"Percentage": "བརྒྱ་ཆ།",
@@ -1725,10 +1726,6 @@
"Auto Sync": "རང་འགུལ་སྤྲི་དོན།",
"Purged book data: {{title}}": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་བསུབ་ཟིན། {{title}}",
"Failed to purge book data: {{title}}": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་བསུབ་མ་ཐུབ། {{title}}",
"Purge Book Data": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "འདིས་དཔེ་དེབ་དང་ཀློག་འདོན་གྱི་འཕེལ་རིམ། ཟིན་ཐོ། དེབ་རྟགས་བཅས་ཀྱི་གཞི་གྲངས་ཡོངས་རྫོགས་གཏན་འཁེལ་དུ་བསུབ་པ་ཡིན། འདི་ཕྱིར་བསྒྱུར་མི་ཐུབ།",
"Purge Data": "གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"Erase the book and all its data, including reading progress": "དཔེ་དེབ་དང་ཀློག་འདོན་གྱི་འཕེལ་རིམ་བཅས་ཀྱི་གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"More Actions": "བྱ་སྤྱོད་གཞན་དག",
"Sign in and make the book available to share it": "ནང་འཛུལ་བྱས་ནས་དཔེ་དེབ་མཁོ་སྤྲོད་རུང་བར་བཟོས་ནས་མཉམ་སྤྱོད་བྱོས།",
"Download the book to export it": "ཕྱིར་འདྲེན་བྱེད་པར་དཔེ་དེབ་ཕབ་ལེན་བྱོས།",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "པར་རིས་ཉར་ཚགས་བྱས་མ་ཐུབ",
"Share Image": "པར་རིས་མཉམ་སྤྱོད།",
"Save Image": "པར་རིས་ཉར་ཚགས།",
"Image saved to gallery": "པར་རིས་པར་མཛོད་ནང་ཉར་ཚགས་བྱས་ཟིན"
"Image saved to gallery": "པར་རིས་པར་མཛོད་ནང་ཉར་ཚགས་བྱས་ཟིན",
"Please select a whole word or uncheck the \"Whole word\" option.": "ཡིག་འབྲུ་ཆ་ཚང་ཞིག་འདེམས་རོགས། ཡང་ན་ \"ཚིག་གྲུབ་ཆ་ཚང་\" ཞེས་པའི་གདམ་ག་ཕྱིར་འཐེན་གནང་རོགས།",
"Regex:": "Regex:",
"Invalid regular expression": "Regex ནུས་མེད་རེད།",
"Find pattern cannot be empty": "འཚོལ་བཤེར་དཔེ་རིས་སྟོང་པ་ཡིན་མི་ཆོག",
"Add Rule": "སྒྲིག་གཞི་ཁ་སྣོན།",
"Find...": "འཚོལ་བཤེར།...",
"Replace with...": "བརྗེ་བ།...",
"Purge & Delete": "གཙང་སེལ་བྱས་ནས་སུབ་པ།",
"Purge all reading data": "ཀློག་པའི་གཞི་གྲངས་ཡོངས་གཙང་སེལ།",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "འདིས་ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་གཏན་དུ་སུབ་ངེས་ཡིན། ཕྱིར་ལྡོག་བྱེད་མི་ཐུབ།",
"Also erase reading progress, notes, and bookmarks.": "ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་ཀྱང་སུབ་པ།"
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "TTS wird für dieses Dokument nicht unterstützt",
"Reset Password": "Passwort zurücksetzen",
"Show Reading Progress": "Lesefortschritt anzeigen",
"Show Progress Bar": "Fortschrittsleiste anzeigen",
"Reading Progress Style": "Lesefortschritt-Stil",
"Page Number": "Seitenzahl",
"Percentage": "Prozentsatz",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Automatische Synchronisierung",
"Purged book data: {{title}}": "Buchdaten gelöscht: {{title}}",
"Failed to purge book data: {{title}}": "Buchdaten konnten nicht gelöscht werden: {{title}}",
"Purge Book Data": "Buchdaten endgültig löschen",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Dadurch werden das Buch und alle zugehörigen Daten einschließlich Lesefortschritt, Notizen und Lesezeichen endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.",
"Purge Data": "Alle Daten löschen",
"Erase the book and all its data, including reading progress": "Buch und alle Daten löschen, einschließlich Lesefortschritt",
"More Actions": "Weitere Aktionen",
"Sign in and make the book available to share it": "Melde dich an und mache das Buch verfügbar, um es zu teilen",
"Download the book to export it": "Lade das Buch herunter, um es zu exportieren",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Bild konnte nicht gespeichert werden",
"Share Image": "Bild teilen",
"Save Image": "Bild speichern",
"Image saved to gallery": "Bild in der Galerie gespeichert"
"Image saved to gallery": "Bild in der Galerie gespeichert",
"Please select a whole word or uncheck the \"Whole word\" option.": "Bitte wählen Sie ein ganzes Wort aus oder deaktivieren Sie die Option „Ganzes Wort\".",
"Regex:": "Regex:",
"Invalid regular expression": "Ungültiger regulärer Ausdruck",
"Find pattern cannot be empty": "Suchmuster darf nicht leer sein",
"Add Rule": "Regel hinzufügen",
"Find...": "Suchen…",
"Replace with...": "Ersetzen durch…",
"Purge & Delete": "Bereinigen und löschen",
"Purge all reading data": "Alle Lesedaten bereinigen",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Dadurch werden Lesefortschritt, Notizen und Lesezeichen dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"Also erase reading progress, notes, and bookmarks.": "Löscht auch Lesefortschritt, Notizen und Lesezeichen."
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "Η τεχνολογία TTS δεν υποστηρίζεται για αυτό το έγγραφο.",
"Reset Password": "Επαναφορά κωδικού",
"Show Reading Progress": "Εμφάνιση προόδου ανάγνωσης",
"Show Progress Bar": "Εμφάνιση γραμμής προόδου",
"Reading Progress Style": "Στυλ προόδου ανάγνωσης",
"Page Number": "Αριθμός σελίδας",
"Percentage": "Ποσοστό",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Αυτόματος συγχρονισμός",
"Purged book data: {{title}}": "Τα δεδομένα του βιβλίου διαγράφηκαν: {{title}}",
"Failed to purge book data: {{title}}": "Αποτυχία διαγραφής δεδομένων βιβλίου: {{title}}",
"Purge Book Data": "Διαγραφή δεδομένων βιβλίου",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Αυτό διαγράφει οριστικά το βιβλίο και όλα τα δεδομένα του, συμπεριλαμβανομένης της προόδου ανάγνωσης, των σημειώσεων και των σελιδοδεικτών. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"Purge Data": "Διαγραφή όλων των δεδομένων",
"Erase the book and all its data, including reading progress": "Διαγράφει το βιβλίο και όλα τα δεδομένα του, συμπεριλαμβανομένης της προόδου ανάγνωσης",
"More Actions": "Περισσότερες ενέργειες",
"Sign in and make the book available to share it": "Συνδεθείτε και κάντε το βιβλίο διαθέσιμο για να το μοιραστείτε",
"Download the book to export it": "Κατεβάστε το βιβλίο για να το εξαγάγετε",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Αποτυχία αποθήκευσης της εικόνας",
"Share Image": "Κοινή χρήση εικόνας",
"Save Image": "Αποθήκευση εικόνας",
"Image saved to gallery": "Η εικόνα αποθηκεύτηκε στη συλλογή"
"Image saved to gallery": "Η εικόνα αποθηκεύτηκε στη συλλογή",
"Please select a whole word or uncheck the \"Whole word\" option.": "Επιλέξτε μια ολόκληρη λέξη ή καταργήστε την επιλογή «Ολόκληρη λέξη».",
"Regex:": "Regex:",
"Invalid regular expression": "Μη έγκυρη κανονική έκφραση",
"Find pattern cannot be empty": "Το μοτίβο αναζήτησης δεν μπορεί να είναι κενό",
"Add Rule": "Προσθήκη κανόνα",
"Find...": "Αναζήτηση…",
"Replace with...": "Αντικατάσταση με…",
"Purge & Delete": "Εκκαθάριση & Διαγραφή",
"Purge all reading data": "Εκκαθάριση όλων των δεδομένων ανάγνωσης",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Αυτό διαγράφει οριστικά την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες. Δεν είναι δυνατή η αναίρεση.",
"Also erase reading progress, notes, and bookmarks.": "Διαγράφει επίσης την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες."
}
@@ -476,6 +476,7 @@
"Reading Progress Style": "Estilo de progreso de lectura",
"Percentage": "Porcentaje",
"Show Reading Progress": "Mostrar progreso de lectura",
"Show Progress Bar": "Mostrar barra de progreso",
"Page Number": "Número de página",
"Deleted local copy of the book: {{title}}": "Se eliminó la copia local del libro: {{title}}",
"Failed to delete cloud backup of the book: {{title}}": "Error al eliminar la copia de seguridad en la nube del libro: {{title}}",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Sincronización automática",
"Purged book data: {{title}}": "Datos del libro borrados: {{title}}",
"Failed to purge book data: {{title}}": "No se pudieron borrar los datos del libro: {{title}}",
"Purge Book Data": "Borrar todos los datos del libro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Esto borra permanentemente el libro y todos sus datos, incluido el progreso de lectura, las notas y los marcadores. Esta acción no se puede deshacer.",
"Purge Data": "Borrar todos los datos",
"Erase the book and all its data, including reading progress": "Borra el libro y todos sus datos, incluido el progreso de lectura",
"More Actions": "Más acciones",
"Sign in and make the book available to share it": "Inicia sesión y ten el libro disponible para compartirlo",
"Download the book to export it": "Descarga el libro para exportarlo",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "No se pudo guardar la imagen",
"Share Image": "Compartir imagen",
"Save Image": "Guardar imagen",
"Image saved to gallery": "Imagen guardada en la galería"
"Image saved to gallery": "Imagen guardada en la galería",
"Please select a whole word or uncheck the \"Whole word\" option.": "Selecciona una palabra completa o desmarca la opción «Palabra completa».",
"Regex:": "Regex:",
"Invalid regular expression": "Expresión regular no válida",
"Find pattern cannot be empty": "El patrón de búsqueda no puede estar vacío",
"Add Rule": "Añadir regla",
"Find...": "Buscar…",
"Replace with...": "Reemplazar por…",
"Purge & Delete": "Purgar y eliminar",
"Purge all reading data": "Purgar todos los datos de lectura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Esto borra permanentemente el progreso de lectura, las notas y los marcadores. Esta acción no se puede deshacer.",
"Also erase reading progress, notes, and bookmarks.": "También borra el progreso de lectura, las notas y los marcadores."
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "قابلیت تبدیل متن به گفتار برای این سند پشتیبانی نمی‌شود",
"Reset Password": "بازنشانی رمز عبور",
"Show Reading Progress": "نمایش وضعیت مطالعه",
"Show Progress Bar": "نمایش نوار پیشرفت",
"Reading Progress Style": "سبک نمایش وضعیت مطالعه",
"Page Number": "شماره صفحه",
"Percentage": "درصد",
@@ -1758,10 +1759,6 @@
"Auto Sync": "همگام‌سازی خودکار",
"Purged book data: {{title}}": "داده‌های کتاب پاک شد: {{title}}",
"Failed to purge book data: {{title}}": "پاک‌سازی داده‌های کتاب ناموفق بود: {{title}}",
"Purge Book Data": "پاک‌سازی داده‌های کتاب",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "این کار کتاب و تمام داده‌های آن، شامل پیشرفت مطالعه، یادداشت‌ها و نشانک‌ها را برای همیشه پاک می‌کند. این عمل قابل بازگشت نیست.",
"Purge Data": "پاک‌سازی داده‌ها",
"Erase the book and all its data, including reading progress": "کتاب و تمام داده‌های آن، شامل پیشرفت مطالعه را پاک می‌کند",
"More Actions": "اقدامات بیشتر",
"Sign in and make the book available to share it": "برای اشتراک‌گذاری، وارد شوید و کتاب را در دسترس قرار دهید",
"Download the book to export it": "برای خروجی گرفتن، کتاب را دانلود کنید",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "ذخیره تصویر ناموفق بود",
"Share Image": "اشتراک‌گذاری تصویر",
"Save Image": "ذخیره تصویر",
"Image saved to gallery": "تصویر در گالری ذخیره شد"
"Image saved to gallery": "تصویر در گالری ذخیره شد",
"Please select a whole word or uncheck the \"Whole word\" option.": "لطفاً یک کلمه کامل انتخاب کنید یا گزینه «کلمه کامل» را غیرفعال کنید.",
"Regex:": "Regex:",
"Invalid regular expression": "عبارت باقاعده نامعتبر است",
"Find pattern cannot be empty": "الگوی جستجو نمی‌تواند خالی باشد",
"Add Rule": "افزودن قاعده",
"Find...": "جستجو…",
"Replace with...": "جایگزین با…",
"Purge & Delete": "پاک‌سازی و حذف",
"Purge all reading data": "پاک‌سازی همه داده‌های مطالعه",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "این کار وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را برای همیشه پاک می‌کند. این عمل قابل بازگشت نیست.",
"Also erase reading progress, notes, and bookmarks.": "همچنین وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را پاک می‌کند."
}
@@ -452,6 +452,7 @@
"TTS not supported for this document": "TTS non pris en charge pour ce document",
"Reset Password": "Nouveau mot de passe",
"Show Reading Progress": "Afficher la progression",
"Show Progress Bar": "Afficher la barre de progression",
"Reading Progress Style": "Style de progression",
"Page Number": "Numéro de page",
"Percentage": "Pourcentage",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Synchronisation automatique",
"Purged book data: {{title}}": "Données du livre purgées : {{title}}",
"Failed to purge book data: {{title}}": "Échec de la purge des données du livre : {{title}}",
"Purge Book Data": "Purger les données du livre",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Cette action supprime définitivement le livre et toutes ses données, y compris la progression de lecture, les notes et les signets. Cette action est irréversible.",
"Purge Data": "Purger les données",
"Erase the book and all its data, including reading progress": "Supprime le livre et toutes ses données, y compris la progression de lecture",
"More Actions": "Plus d'actions",
"Sign in and make the book available to share it": "Connectez-vous et rendez le livre disponible pour le partager",
"Download the book to export it": "Téléchargez le livre pour l'exporter",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "Échec de l'enregistrement de l'image",
"Share Image": "Partager l'image",
"Save Image": "Enregistrer l'image",
"Image saved to gallery": "Image enregistrée dans la galerie"
"Image saved to gallery": "Image enregistrée dans la galerie",
"Please select a whole word or uncheck the \"Whole word\" option.": "Veuillez sélectionner un mot entier ou décocher loption « Mot entier ».",
"Regex:": "Regex :",
"Invalid regular expression": "Expression régulière invalide",
"Find pattern cannot be empty": "Le motif de recherche ne peut pas être vide",
"Add Rule": "Ajouter une règle",
"Find...": "Rechercher…",
"Replace with...": "Remplacer par…",
"Purge & Delete": "Purger et supprimer",
"Purge all reading data": "Purger toutes les données de lecture",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Cela efface définitivement la progression de lecture, les notes et les signets. Cette action est irréversible.",
"Also erase reading progress, notes, and bookmarks.": "Efface aussi la progression de lecture, les notes et les signets."
}
@@ -950,6 +950,7 @@
"Show Remaining Time": "הצג זמן נותר",
"Show Remaining Pages": "הצג דפים נותרים",
"Show Reading Progress": "הצג התקדמות קריאה",
"Show Progress Bar": "הצג סרגל התקדמות",
"Reading Progress Style": "סגנון התקדמות קריאה",
"Page Number": "מספר דף",
"Percentage": "אחוזים",
@@ -1791,10 +1792,6 @@
"Auto Sync": "סנכרון אוטומטי",
"Purged book data: {{title}}": "נתוני הספר נמחקו: {{title}}",
"Failed to purge book data: {{title}}": "מחיקת נתוני הספר נכשלה: {{title}}",
"Purge Book Data": "מחיקת כל נתוני הספר",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "פעולה זו מוחקת לצמיתות את הספר ואת כל הנתונים שלו, כולל התקדמות הקריאה, ההערות והסימניות. לא ניתן לבטל פעולה זו.",
"Purge Data": "מחק את כל הנתונים",
"Erase the book and all its data, including reading progress": "מחיקת הספר וכל הנתונים שלו, כולל התקדמות הקריאה",
"More Actions": "פעולות נוספות",
"Sign in and make the book available to share it": "היכנס והפוך את הספר לזמין כדי לשתף אותו",
"Download the book to export it": "הורד את הספר כדי לייצא אותו",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "שמירת התמונה נכשלה",
"Share Image": "שיתוף התמונה",
"Save Image": "שמירת התמונה",
"Image saved to gallery": "התמונה נשמרה בגלריה"
"Image saved to gallery": "התמונה נשמרה בגלריה",
"Please select a whole word or uncheck the \"Whole word\" option.": "יש לבחור מילה שלמה או לבטל את האפשרות \"מילה שלמה\".",
"Regex:": "Regex:",
"Invalid regular expression": "ביטוי רגולרי לא תקין",
"Find pattern cannot be empty": "תבנית החיפוש לא יכולה להיות ריקה",
"Add Rule": "הוסף כלל",
"Find...": "חיפוש...",
"Replace with...": "החלף ב...",
"Purge & Delete": "נקה ומחק",
"Purge all reading data": "נקה את כל נתוני הקריאה",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "פעולה זו מוחקת לצמיתות את התקדמות הקריאה, ההערות והסימניות. לא ניתן לבטל פעולה זו.",
"Also erase reading progress, notes, and bookmarks.": "מוחק גם את התקדמות הקריאה, ההערות והסימניות."
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "TTS इस दस्तावेज़ के लिए समर्थित नहीं है",
"Reset Password": "पासवर्ड रीसेट करें",
"Show Reading Progress": "पढ़ने की प्रगति दिखाएं",
"Show Progress Bar": "प्रगति पट्टी दिखाएं",
"Reading Progress Style": "पढ़ने की प्रगति शैली",
"Page Number": "पृष्ठ संख्या",
"Percentage": "प्रतिशत",
@@ -1758,10 +1759,6 @@
"Auto Sync": "स्वतः सिंक",
"Purged book data: {{title}}": "पुस्तक का डेटा मिटाया गया: {{title}}",
"Failed to purge book data: {{title}}": "पुस्तक का डेटा मिटाने में विफल: {{title}}",
"Purge Book Data": "पुस्तक का सारा डेटा मिटाएँ",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "इससे पुस्तक और उसका सारा डेटा, जिसमें पढ़ने की प्रगति, नोट्स और बुकमार्क शामिल हैं, स्थायी रूप से मिट जाएगा। इसे पूर्ववत नहीं किया जा सकता।",
"Purge Data": "सारा डेटा मिटाएँ",
"Erase the book and all its data, including reading progress": "पढ़ने की प्रगति सहित पुस्तक और उसका सारा डेटा मिटाएँ",
"More Actions": "अधिक क्रियाएँ",
"Sign in and make the book available to share it": "साझा करने के लिए साइन इन करें और पुस्तक उपलब्ध कराएँ",
"Download the book to export it": "निर्यात करने के लिए पुस्तक डाउनलोड करें",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "छवि सहेजने में विफल",
"Share Image": "छवि साझा करें",
"Save Image": "छवि सहेजें",
"Image saved to gallery": "छवि गैलरी में सहेजी गई"
"Image saved to gallery": "छवि गैलरी में सहेजी गई",
"Please select a whole word or uncheck the \"Whole word\" option.": "कृपया पूरा शब्द चुनें या \"पूरा शब्द\" विकल्प को अनचेक करें।",
"Regex:": "Regex:",
"Invalid regular expression": "अमान्य रेगुलर एक्सप्रेशन",
"Find pattern cannot be empty": "खोज पैटर्न खाली नहीं हो सकता",
"Add Rule": "नियम जोड़ें",
"Find...": "खोजें…",
"Replace with...": "से बदलें…",
"Purge & Delete": "साफ़ करें और हटाएं",
"Purge all reading data": "सभी पढ़ने का डेटा साफ़ करें",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "यह पढ़ने की प्रगति, नोट्स और बुकमार्क को स्थायी रूप से मिटा देता है। इसे पूर्ववत नहीं किया जा सकता।",
"Also erase reading progress, notes, and bookmarks.": "पढ़ने की प्रगति, नोट्स और बुकमार्क भी मिटा देता है।"
}
@@ -1027,6 +1027,7 @@
"Show Remaining Time": "Hátralévő idő megjelenítése",
"Show Remaining Pages": "Hátralévő oldalak megjelenítése",
"Show Reading Progress": "Olvasási haladás megjelenítése",
"Show Progress Bar": "Folyamatjelző megjelenítése",
"Reading Progress Style": "Olvasási haladás stílusa",
"Percentage": "Százalék",
"Show Current Time": "Aktuális idő megjelenítése",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Automatikus szinkronizálás",
"Purged book data: {{title}}": "Könyv adatai törölve: {{title}}",
"Failed to purge book data: {{title}}": "A könyv adatainak törlése sikertelen: {{title}}",
"Purge Book Data": "Könyv összes adatának törlése",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ez véglegesen törli a könyvet és minden adatát, beleértve az olvasási előrehaladást, a jegyzeteket és a könyvjelzőket. A művelet nem vonható vissza.",
"Purge Data": "Összes adat törlése",
"Erase the book and all its data, including reading progress": "Törli a könyvet és minden adatát, beleértve az olvasási előrehaladást",
"More Actions": "További műveletek",
"Sign in and make the book available to share it": "Jelentkezz be, és tedd elérhetővé a könyvet a megosztáshoz",
"Download the book to export it": "Töltsd le a könyvet az exportáláshoz",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Nem sikerült menteni a képet",
"Share Image": "Kép megosztása",
"Save Image": "Kép mentése",
"Image saved to gallery": "A kép elmentve a galériába"
"Image saved to gallery": "A kép elmentve a galériába",
"Please select a whole word or uncheck the \"Whole word\" option.": "Válasszon ki egy teljes szót, vagy törölje a „Teljes szó\" beállítás jelölését.",
"Regex:": "Regex:",
"Invalid regular expression": "Érvénytelen reguláris kifejezés",
"Find pattern cannot be empty": "A keresési minta nem lehet üres",
"Add Rule": "Szabály hozzáadása",
"Find...": "Keresés...",
"Replace with...": "Csere erre...",
"Purge & Delete": "Tisztítás és törlés",
"Purge all reading data": "Összes olvasási adat törlése",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ez véglegesen törli az olvasási haladást, a jegyzeteket és a könyvjelzőket. A művelet nem vonható vissza.",
"Also erase reading progress, notes, and bookmarks.": "Az olvasási haladást, jegyzeteket és könyvjelzőket is törli."
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "TTS tidak didukung untuk dokumen ini",
"Reset Password": "Reset Kata Sandi",
"Show Reading Progress": "Perlihatkan Progres Membaca",
"Show Progress Bar": "Tampilkan Bilah Progres",
"Reading Progress Style": "Gaya Progres Membaca",
"Page Number": "Nomor Halaman",
"Percentage": "Persentase",
@@ -1725,10 +1726,6 @@
"Auto Sync": "Sinkronisasi Otomatis",
"Purged book data: {{title}}": "Data buku dihapus: {{title}}",
"Failed to purge book data: {{title}}": "Gagal menghapus data buku: {{title}}",
"Purge Book Data": "Hapus semua data buku",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ini akan menghapus buku secara permanen beserta semua datanya, termasuk progres baca, catatan, dan markah. Tindakan ini tidak dapat dibatalkan.",
"Purge Data": "Hapus semua data",
"Erase the book and all its data, including reading progress": "Menghapus buku dan semua datanya, termasuk progres baca",
"More Actions": "Tindakan lainnya",
"Sign in and make the book available to share it": "Masuk dan sediakan buku untuk membagikannya",
"Download the book to export it": "Unduh buku untuk mengekspornya",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "Gagal menyimpan gambar",
"Share Image": "Bagikan Gambar",
"Save Image": "Simpan Gambar",
"Image saved to gallery": "Gambar disimpan ke galeri"
"Image saved to gallery": "Gambar disimpan ke galeri",
"Please select a whole word or uncheck the \"Whole word\" option.": "Pilih satu kata utuh atau hapus centang opsi \"Kata utuh\".",
"Regex:": "Regex:",
"Invalid regular expression": "Ekspresi reguler tidak valid",
"Find pattern cannot be empty": "Pola pencarian tidak boleh kosong",
"Add Rule": "Tambah aturan",
"Find...": "Cari…",
"Replace with...": "Ganti dengan…",
"Purge & Delete": "Bersihkan & Hapus",
"Purge all reading data": "Bersihkan semua data baca",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini menghapus permanen progres membaca, catatan, dan penanda. Tindakan ini tidak dapat dibatalkan.",
"Also erase reading progress, notes, and bookmarks.": "Juga menghapus progres membaca, catatan, dan penanda."
}
@@ -452,6 +452,7 @@
"TTS not supported for this document": "TTS non supportato per questo documento",
"Reset Password": "Reimposta Password",
"Show Reading Progress": "Mostra Progresso Lettura",
"Show Progress Bar": "Mostra barra di avanzamento",
"Reading Progress Style": "Stile di Progresso Lettura",
"Page Number": "Numero di Pagina",
"Percentage": "Percentuale",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Sincronizzazione automatica",
"Purged book data: {{title}}": "Dati del libro eliminati: {{title}}",
"Failed to purge book data: {{title}}": "Impossibile eliminare i dati del libro: {{title}}",
"Purge Book Data": "Elimina tutti i dati del libro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Questa operazione elimina definitivamente il libro e tutti i suoi dati, inclusi i progressi di lettura, le note e i segnalibri. L'operazione non può essere annullata.",
"Purge Data": "Elimina tutti i dati",
"Erase the book and all its data, including reading progress": "Elimina il libro e tutti i suoi dati, inclusi i progressi di lettura",
"More Actions": "Altre azioni",
"Sign in and make the book available to share it": "Accedi e rendi disponibile il libro per condividerlo",
"Download the book to export it": "Scarica il libro per esportarlo",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "Impossibile salvare l'immagine",
"Share Image": "Condividi immagine",
"Save Image": "Salva immagine",
"Image saved to gallery": "Immagine salvata nella galleria"
"Image saved to gallery": "Immagine salvata nella galleria",
"Please select a whole word or uncheck the \"Whole word\" option.": "Seleziona una parola intera o deseleziona lopzione «Parola intera».",
"Regex:": "Regex:",
"Invalid regular expression": "Espressione regolare non valida",
"Find pattern cannot be empty": "Il criterio di ricerca non può essere vuoto",
"Add Rule": "Aggiungi regola",
"Find...": "Cerca…",
"Replace with...": "Sostituisci con…",
"Purge & Delete": "Svuota ed elimina",
"Purge all reading data": "Elimina tutti i dati di lettura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Questa operazione cancella definitivamente il progresso di lettura, le note e i segnalibri. Non può essere annullata.",
"Also erase reading progress, notes, and bookmarks.": "Cancella anche il progresso di lettura, le note e i segnalibri."
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "TTSはこのドキュメントではサポートされていません",
"Reset Password": "パスワードをリセット",
"Show Reading Progress": "読書進捗を表示",
"Show Progress Bar": "進捗バーを表示",
"Reading Progress Style": "読書進捗スタイル",
"Page Number": "ページ番号",
"Percentage": "パーセンテージ",
@@ -1725,10 +1726,6 @@
"Auto Sync": "自動同期",
"Purged book data: {{title}}": "書籍データを消去しました: {{title}}",
"Failed to purge book data: {{title}}": "書籍データの消去に失敗しました: {{title}}",
"Purge Book Data": "書籍データを消去",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "これにより、本と読書の進行状況・メモ・しおりを含むすべてのデータが完全に削除されます。この操作は取り消せません。",
"Purge Data": "データを消去",
"Erase the book and all its data, including reading progress": "本と読書の進行状況を含むすべてのデータを消去します",
"More Actions": "その他の操作",
"Sign in and make the book available to share it": "共有するにはサインインして書籍を利用可能にしてください",
"Download the book to export it": "エクスポートするには書籍をダウンロードしてください",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "画像の保存に失敗しました",
"Share Image": "画像を共有",
"Save Image": "画像を保存",
"Image saved to gallery": "画像をギャラリーに保存しました"
"Image saved to gallery": "画像をギャラリーに保存しました",
"Please select a whole word or uncheck the \"Whole word\" option.": "単語全体を選択するか、「単語全体」オプションのチェックを外してください。",
"Regex:": "正規表現:",
"Invalid regular expression": "正規表現が無効です",
"Find pattern cannot be empty": "検索パターンを空にできません",
"Add Rule": "ルールを追加",
"Find...": "検索…",
"Replace with...": "置換後…",
"Purge & Delete": "消去して削除",
"Purge all reading data": "すべての読書データを消去",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "読書進捗、メモ、ブックマークが完全に消去されます。この操作は元に戻せません。",
"Also erase reading progress, notes, and bookmarks.": "読書進捗、メモ、ブックマークも消去します。"
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "TTS가 이 문서에서 지원되지 않습니다",
"Reset Password": "비밀번호 재설정",
"Show Reading Progress": "읽기 진행 상황 표시",
"Show Progress Bar": "진행률 표시줄 표시",
"Reading Progress Style": "읽기 진행 상황 스타일",
"Page Number": "페이지 번호",
"Percentage": "백분율",
@@ -1725,10 +1726,6 @@
"Auto Sync": "자동 동기화",
"Purged book data: {{title}}": "도서 데이터를 삭제했습니다: {{title}}",
"Failed to purge book data: {{title}}": "도서 데이터 삭제에 실패했습니다: {{title}}",
"Purge Book Data": "도서 데이터 완전 삭제",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "책과 읽기 진행률, 메모, 책갈피를 포함한 모든 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
"Purge Data": "데이터 완전 삭제",
"Erase the book and all its data, including reading progress": "읽기 진행률을 포함한 책과 모든 데이터를 삭제합니다",
"More Actions": "추가 작업",
"Sign in and make the book available to share it": "공유하려면 로그인하고 책을 사용할 수 있도록 하세요",
"Download the book to export it": "내보내려면 책을 다운로드하세요",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "이미지를 저장하지 못했습니다",
"Share Image": "이미지 공유",
"Save Image": "이미지 저장",
"Image saved to gallery": "이미지를 갤러리에 저장했습니다"
"Image saved to gallery": "이미지를 갤러리에 저장했습니다",
"Please select a whole word or uncheck the \"Whole word\" option.": "전체 단어를 선택하거나 '전체 단어' 옵션의 선택을 해제하세요.",
"Regex:": "정규식:",
"Invalid regular expression": "잘못된 정규식입니다",
"Find pattern cannot be empty": "찾을 내용을 비워 둘 수 없습니다",
"Add Rule": "규칙 추가",
"Find...": "검색…",
"Replace with...": "바꿀 내용…",
"Purge & Delete": "데이터 지우고 삭제",
"Purge all reading data": "모든 읽기 데이터 지우기",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "읽기 진행 상황, 메모, 북마크가 영구적으로 지워집니다. 이 작업은 되돌릴 수 없습니다.",
"Also erase reading progress, notes, and bookmarks.": "읽기 진행 상황, 메모, 북마크도 함께 지웁니다."
}
@@ -594,6 +594,7 @@
"Show Remaining Time": "Tunjuk Masa Berbaki",
"Show Remaining Pages": "Tunjuk Halaman Berbaki",
"Show Reading Progress": "Tunjuk Kemajuan Membaca",
"Show Progress Bar": "Tunjuk Bar Kemajuan",
"Reading Progress Style": "Gaya Kemajuan Membaca",
"Page Number": "Nombor Halaman",
"Percentage": "Peratusan",
@@ -1725,10 +1726,6 @@
"Auto Sync": "Segerak Automatik",
"Purged book data: {{title}}": "Data buku dipadam: {{title}}",
"Failed to purge book data: {{title}}": "Gagal memadam data buku: {{title}}",
"Purge Book Data": "Padam semua data buku",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ini memadamkan buku dan semua datanya secara kekal, termasuk kemajuan bacaan, nota dan penanda buku. Tindakan ini tidak boleh dibuat asal.",
"Purge Data": "Padam semua data",
"Erase the book and all its data, including reading progress": "Memadam buku dan semua datanya, termasuk kemajuan bacaan",
"More Actions": "Tindakan lain",
"Sign in and make the book available to share it": "Log masuk dan sediakan buku untuk berkongsinya",
"Download the book to export it": "Muat turun buku untuk mengeksportnya",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "Gagal menyimpan imej",
"Share Image": "Kongsi Imej",
"Save Image": "Simpan Imej",
"Image saved to gallery": "Imej disimpan ke galeri"
"Image saved to gallery": "Imej disimpan ke galeri",
"Please select a whole word or uncheck the \"Whole word\" option.": "Sila pilih perkataan penuh atau nyahtanda pilihan \"Perkataan penuh\".",
"Regex:": "Regex:",
"Invalid regular expression": "Ungkapan biasa tidak sah",
"Find pattern cannot be empty": "Corak carian tidak boleh kosong",
"Add Rule": "Tambah peraturan",
"Find...": "Cari…",
"Replace with...": "Ganti dengan…",
"Purge & Delete": "Bersihkan & Padam",
"Purge all reading data": "Bersihkan semua data bacaan",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini memadam kemajuan membaca, nota dan penanda buku secara kekal. Tindakan ini tidak boleh dibatalkan.",
"Also erase reading progress, notes, and bookmarks.": "Turut memadam kemajuan membaca, nota dan penanda buku."
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "TTS niet ondersteund voor dit document",
"Reset Password": "Wachtwoord resetten",
"Show Reading Progress": "Leesvoortgang weergeven",
"Show Progress Bar": "Voortgangsbalk weergeven",
"Reading Progress Style": "Stijl van leesvoortgang",
"Page Number": "Pagina nummer",
"Percentage": "Percentage",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Automatische synchronisatie",
"Purged book data: {{title}}": "Boekgegevens gewist: {{title}}",
"Failed to purge book data: {{title}}": "Kan boekgegevens niet wissen: {{title}}",
"Purge Book Data": "Alle boekgegevens wissen",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Hiermee worden het boek en alle bijbehorende gegevens, waaronder leesvoortgang, notities en bladwijzers, permanent gewist. Dit kan niet ongedaan worden gemaakt.",
"Purge Data": "Alle gegevens wissen",
"Erase the book and all its data, including reading progress": "Wist het boek en alle gegevens, inclusief leesvoortgang",
"More Actions": "Meer acties",
"Sign in and make the book available to share it": "Meld je aan en maak het boek beschikbaar om het te delen",
"Download the book to export it": "Download het boek om het te exporteren",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Kan de afbeelding niet opslaan",
"Share Image": "Afbeelding delen",
"Save Image": "Afbeelding opslaan",
"Image saved to gallery": "Afbeelding opgeslagen in de galerij"
"Image saved to gallery": "Afbeelding opgeslagen in de galerij",
"Please select a whole word or uncheck the \"Whole word\" option.": "Selecteer een heel woord of schakel de optie Heel woord uit.",
"Regex:": "Regex:",
"Invalid regular expression": "Ongeldige reguliere expressie",
"Find pattern cannot be empty": "Zoekpatroon mag niet leeg zijn",
"Add Rule": "Regel toevoegen",
"Find...": "Zoeken…",
"Replace with...": "Vervangen door…",
"Purge & Delete": "Wissen en verwijderen",
"Purge all reading data": "Alle leesgegevens wissen",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Hiermee worden leesvoortgang, notities en bladwijzers permanent gewist. Dit kan niet ongedaan worden gemaakt.",
"Also erase reading progress, notes, and bookmarks.": "Wist ook leesvoortgang, notities en bladwijzers."
}
@@ -456,6 +456,7 @@
"TTS not supported for this document": "TTS nie jest obsługiwane dla tego dokumentu",
"Reset Password": "Zresetuj hasło",
"Show Reading Progress": "Pokazuj postęp czytania",
"Show Progress Bar": "Pokaż pasek postępu",
"Reading Progress Style": "Styl postępu czytania",
"Page Number": "Numer strony",
"Percentage": "Procent",
@@ -1824,10 +1825,6 @@
"Auto Sync": "Automatyczna synchronizacja",
"Purged book data: {{title}}": "Usunięto dane książki: {{title}}",
"Failed to purge book data: {{title}}": "Nie udało się usunąć danych książki: {{title}}",
"Purge Book Data": "Usuń wszystkie dane książki",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Spowoduje to trwałe usunięcie książki i wszystkich jej danych, w tym postępu czytania, notatek i zakładek. Tej operacji nie można cofnąć.",
"Purge Data": "Usuń wszystkie dane",
"Erase the book and all its data, including reading progress": "Usuwa książkę i wszystkie jej dane, w tym postęp czytania",
"More Actions": "Więcej akcji",
"Sign in and make the book available to share it": "Zaloguj się i pobierz książkę, aby ją udostępnić",
"Download the book to export it": "Pobierz książkę, aby ją wyeksportować",
@@ -1846,5 +1843,16 @@
"Failed to save the image": "Nie udało się zapisać obrazu",
"Share Image": "Udostępnij obraz",
"Save Image": "Zapisz obraz",
"Image saved to gallery": "Obraz zapisano w galerii"
"Image saved to gallery": "Obraz zapisano w galerii",
"Please select a whole word or uncheck the \"Whole word\" option.": "Zaznacz całe słowo lub odznacz opcję „Całe słowo\".",
"Regex:": "Regex:",
"Invalid regular expression": "Nieprawidłowe wyrażenie regularne",
"Find pattern cannot be empty": "Wzorzec wyszukiwania nie może być pusty",
"Add Rule": "Dodaj regułę",
"Find...": "Szukaj…",
"Replace with...": "Zastąp przez…",
"Purge & Delete": "Wyczyść i usuń",
"Purge all reading data": "Wyczyść wszystkie dane czytania",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Spowoduje to trwałe usunięcie postępu czytania, notatek i zakładek. Tej operacji nie można cofnąć.",
"Also erase reading progress, notes, and bookmarks.": "Usuwa również postęp czytania, notatki i zakładki."
}
@@ -1177,6 +1177,7 @@
"Show Remaining Time": "Mostrar tempo restante",
"Show Remaining Pages": "Mostrar páginas restantes",
"Show Reading Progress": "Mostrar progresso de leitura",
"Show Progress Bar": "Mostrar barra de progresso",
"Reading Progress Style": "Estilo do progresso de leitura",
"Percentage": "Porcentagem",
"Show Current Time": "Mostrar hora atual",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Sincronização automática",
"Purged book data: {{title}}": "Dados do livro apagados: {{title}}",
"Failed to purge book data: {{title}}": "Falha ao apagar os dados do livro: {{title}}",
"Purge Book Data": "Apagar todos os dados do livro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Isso apaga permanentemente o livro e todos os seus dados, incluindo o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Purge Data": "Apagar todos os dados",
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
"More Actions": "Mais ações",
"Sign in and make the book available to share it": "Faça login e tenha o livro disponível para compartilhá-lo",
"Download the book to export it": "Baixe o livro para exportá-lo",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "Falha ao salvar a imagem",
"Share Image": "Compartilhar imagem",
"Save Image": "Salvar imagem",
"Image saved to gallery": "Imagem salva na galeria"
"Image saved to gallery": "Imagem salva na galeria",
"Please select a whole word or uncheck the \"Whole word\" option.": "Selecione uma palavra inteira ou desmarque a opção \"Palavra inteira\".",
"Regex:": "Regex:",
"Invalid regular expression": "Expressão regular inválida",
"Find pattern cannot be empty": "O padrão de busca não pode ficar vazio",
"Add Rule": "Adicionar regra",
"Find...": "Buscar...",
"Replace with...": "Substituir por...",
"Purge & Delete": "Limpar e excluir",
"Purge all reading data": "Limpar todos os dados de leitura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isso apaga permanentemente o progresso de leitura, as notas e os marcadores. Esta ação não pode ser desfeita.",
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, as notas e os marcadores."
}
@@ -452,6 +452,7 @@
"TTS not supported for this document": "TTS não suportado para este documento",
"Reset Password": "Redefinir Senha",
"Show Reading Progress": "Mostrar Progresso de Leitura",
"Show Progress Bar": "Mostrar barra de progresso",
"Reading Progress Style": "Estilo de Progresso de Leitura",
"Page Number": "Número da Página",
"Percentage": "Porcentagem",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Sincronização Automática",
"Purged book data: {{title}}": "Dados do livro apagados: {{title}}",
"Failed to purge book data: {{title}}": "Falha ao apagar os dados do livro: {{title}}",
"Purge Book Data": "Apagar todos os dados do livro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Isto apaga permanentemente o livro e todos os seus dados, incluindo o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Purge Data": "Apagar todos os dados",
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
"More Actions": "Mais ações",
"Sign in and make the book available to share it": "Inicie sessão e tenha o livro disponível para o partilhar",
"Download the book to export it": "Transfira o livro para o exportar",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "Falha ao salvar a imagem",
"Share Image": "Partilhar imagem",
"Save Image": "Salvar imagem",
"Image saved to gallery": "Imagem guardada na galeria"
"Image saved to gallery": "Imagem guardada na galeria",
"Please select a whole word or uncheck the \"Whole word\" option.": "Selecione uma palavra inteira ou desmarque a opção «Palavra inteira».",
"Regex:": "Regex:",
"Invalid regular expression": "Expressão regular inválida",
"Find pattern cannot be empty": "O padrão de pesquisa não pode estar vazio",
"Add Rule": "Adicionar regra",
"Find...": "Buscar…",
"Replace with...": "Substituir por…",
"Purge & Delete": "Limpar e excluir",
"Purge all reading data": "Limpar todos os dados de leitura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isto apaga permanentemente o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, notas e marcadores."
}
@@ -1016,6 +1016,7 @@
"Show Remaining Time": "Afișează timpul rămas",
"Show Remaining Pages": "Afișează paginile rămase",
"Show Reading Progress": "Afișează progresul citirii",
"Show Progress Bar": "Afișează bara de progres",
"Reading Progress Style": "Stilul progresului citirii",
"Percentage": "Procent",
"Show Current Time": "Afișează ora curentă",
@@ -1791,10 +1792,6 @@
"Auto Sync": "Sincronizare automată",
"Purged book data: {{title}}": "Datele cărții au fost șterse: {{title}}",
"Failed to purge book data: {{title}}": "Ștergerea datelor cărții a eșuat: {{title}}",
"Purge Book Data": "Șterge toate datele cărții",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Aceasta șterge definitiv cartea și toate datele sale, inclusiv progresul lecturii, notițele și marcajele. Această acțiune nu poate fi anulată.",
"Purge Data": "Șterge toate datele",
"Erase the book and all its data, including reading progress": "Șterge cartea și toate datele sale, inclusiv progresul lecturii",
"More Actions": "Mai multe acțiuni",
"Sign in and make the book available to share it": "Conectează-te și fă cartea disponibilă pentru a o partaja",
"Download the book to export it": "Descarcă cartea pentru a o exporta",
@@ -1813,5 +1810,16 @@
"Failed to save the image": "Salvarea imaginii a eșuat",
"Share Image": "Partajează imaginea",
"Save Image": "Salvează imaginea",
"Image saved to gallery": "Imagine salvată în galerie"
"Image saved to gallery": "Imagine salvată în galerie",
"Please select a whole word or uncheck the \"Whole word\" option.": "Selectați un cuvânt întreg sau debifați opțiunea „Cuvântul întreg\".",
"Regex:": "Regex:",
"Invalid regular expression": "Expresie regulată invalidă",
"Find pattern cannot be empty": "Modelul de căutare nu poate fi gol",
"Add Rule": "Adaugă regulă",
"Find...": "Căutare...",
"Replace with...": "Înlocuiește cu...",
"Purge & Delete": "Curăță și șterge",
"Purge all reading data": "Curăță toate datele de citire",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Aceasta șterge definitiv progresul citirii, notele și marcajele. Această acțiune nu poate fi anulată.",
"Also erase reading progress, notes, and bookmarks.": "Șterge de asemenea progresul citirii, notele și marcajele."
}
@@ -456,6 +456,7 @@
"TTS not supported for this document": "TTS не поддерживается для этого документа",
"Reset Password": "Сбросить пароль",
"Show Reading Progress": "Показать прогресс чтения",
"Show Progress Bar": "Показать полосу прогресса",
"Reading Progress Style": "Стиль прогресса чтения",
"Page Number": "Номер страницы",
"Percentage": "Процент",
@@ -1824,10 +1825,6 @@
"Auto Sync": "Автосинхронизация",
"Purged book data: {{title}}": "Данные книги очищены: {{title}}",
"Failed to purge book data: {{title}}": "Не удалось очистить данные книги: {{title}}",
"Purge Book Data": "Очистить данные книги",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Это навсегда удалит книгу и все её данные, включая прогресс чтения, заметки и закладки. Это действие нельзя отменить.",
"Purge Data": "Очистить данные",
"Erase the book and all its data, including reading progress": "Удаляет книгу и все её данные, включая прогресс чтения",
"More Actions": "Ещё действия",
"Sign in and make the book available to share it": "Войдите и сделайте книгу доступной, чтобы поделиться ею",
"Download the book to export it": "Скачайте книгу, чтобы экспортировать её",
@@ -1846,5 +1843,16 @@
"Failed to save the image": "Не удалось сохранить изображение",
"Share Image": "Поделиться изображением",
"Save Image": "Сохранить изображение",
"Image saved to gallery": "Изображение сохранено в галерее"
"Image saved to gallery": "Изображение сохранено в галерее",
"Please select a whole word or uncheck the \"Whole word\" option.": "Выберите слово целиком или снимите флажок «Слово целиком».",
"Regex:": "Regex:",
"Invalid regular expression": "Недопустимое регулярное выражение",
"Find pattern cannot be empty": "Шаблон поиска не может быть пустым",
"Add Rule": "Добавить правило",
"Find...": "Поиск…",
"Replace with...": "Заменить на…",
"Purge & Delete": "Очистить и удалить",
"Purge all reading data": "Очистить все данные чтения",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Это безвозвратно удалит прогресс чтения, заметки и закладки. Это действие нельзя отменить.",
"Also erase reading progress, notes, and bookmarks.": "Также удаляет прогресс чтения, заметки и закладки."
}
@@ -267,6 +267,7 @@
"Show Remaining Time": "ඉතිරි වේලාව පෙන්වන්න",
"Show Remaining Pages": "ඉතිරි පිටු පෙන්වන්න",
"Show Reading Progress": "කියවීමේ ප්‍රගතිය පෙන්වන්න",
"Show Progress Bar": "ප්‍රගති තීරුව පෙන්වන්න",
"Reading Progress Style": "කියවීමේ ප්‍රගති ශෛලිය",
"Page Number": "පිටු අංකය",
"Percentage": "ප්‍රතිශතය",
@@ -1758,10 +1759,6 @@
"Auto Sync": "ස්වයංක්‍රීය සමමුහුර්තය",
"Purged book data: {{title}}": "පොතේ දත්ත මකා දමන ලදී: {{title}}",
"Failed to purge book data: {{title}}": "පොතේ දත්ත මැකීමට අසමත් විය: {{title}}",
"Purge Book Data": "පොතේ සියලු දත්ත මකන්න",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "මෙය පොත සහ කියවීමේ ප්‍රගතිය, සටහන් සහ පිටු සලකුණු ඇතුළුව එහි සියලු දත්ත ස්ථිරවම මකා දමයි. මෙය පෙරළිය නොහැක.",
"Purge Data": "සියලු දත්ත මකන්න",
"Erase the book and all its data, including reading progress": "කියවීමේ ප්‍රගතිය ඇතුළුව පොත සහ එහි සියලු දත්ත මකන්න",
"More Actions": "තවත් ක්‍රියා",
"Sign in and make the book available to share it": "බෙදා ගැනීමට පුරනය වී පොත ලබා ගත හැකි කරන්න",
"Download the book to export it": "අපනයනය කිරීමට පොත බාගන්න",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "රූපය සුරැකීමට අසමත් විය",
"Share Image": "රූපය බෙදාගන්න",
"Save Image": "රූපය සුරකින්න",
"Image saved to gallery": "රූපය ගැලරියට සුරැකිණි"
"Image saved to gallery": "රූපය ගැලරියට සුරැකිණි",
"Please select a whole word or uncheck the \"Whole word\" option.": "සම්පූර්ණ වචනයක් තෝරන්න හෝ \"සම්පූර්ණ වචනය\" විකල්පය ඉවත් කරන්න.",
"Regex:": "Regex:",
"Invalid regular expression": "වලංගු නොවන නිත්‍ය ප්‍රකාශනය",
"Find pattern cannot be empty": "සෙවුම් රටාව හිස් විය නොහැක",
"Add Rule": "රීතිය එක් කරන්න",
"Find...": "සොයන්න…",
"Replace with...": "ප්‍රතිස්ථාපනය කරන්න…",
"Purge & Delete": "ඉවත් කර මකන්න",
"Purge all reading data": "සියලු කියවීම් දත්ත ඉවත් කරන්න",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "මෙය කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ස්ථිරවම මකා දමයි. මෙය අහෝසි කළ නොහැක.",
"Also erase reading progress, notes, and bookmarks.": "කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ද මකා දමයි."
}
@@ -991,6 +991,7 @@
"Show Remaining Time": "Priki preostali čas",
"Show Remaining Pages": "Prikaži preostale strani",
"Show Reading Progress": "Prikaži napredek branja",
"Show Progress Bar": "Prikaži vrstico napredka",
"Reading Progress Style": "Slog napredka branja",
"Percentage": "Odstotek",
"Tap to Toggle Footer": "Tapnite za preklop noge",
@@ -1824,10 +1825,6 @@
"Auto Sync": "Samodejna sinhronizacija",
"Purged book data: {{title}}": "Podatki knjige izbrisani: {{title}}",
"Failed to purge book data: {{title}}": "Podatkov knjige ni bilo mogoče izbrisati: {{title}}",
"Purge Book Data": "Izbriši vse podatke knjige",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "S tem trajno izbrišete knjigo in vse njene podatke, vključno z napredkom branja, zapiski in zaznamki. Tega dejanja ni mogoče razveljaviti.",
"Purge Data": "Izbriši vse podatke",
"Erase the book and all its data, including reading progress": "Izbriše knjigo in vse njene podatke, vključno z napredkom branja",
"More Actions": "Več dejanj",
"Sign in and make the book available to share it": "Prijavite se in omogočite knjigo za deljenje",
"Download the book to export it": "Prenesite knjigo, da jo izvozite",
@@ -1846,5 +1843,16 @@
"Failed to save the image": "Slike ni bilo mogoče shraniti",
"Share Image": "Deli sliko",
"Save Image": "Shrani sliko",
"Image saved to gallery": "Slika je shranjena v galerijo"
"Image saved to gallery": "Slika je shranjena v galerijo",
"Please select a whole word or uncheck the \"Whole word\" option.": "Izberite celo besedo ali počistite možnost »Cela beseda«.",
"Regex:": "Regex:",
"Invalid regular expression": "Neveljaven regularni izraz",
"Find pattern cannot be empty": "Iskalni vzorec ne sme biti prazen",
"Add Rule": "Dodaj pravilo",
"Find...": "Iskanje...",
"Replace with...": "Zamenjaj z...",
"Purge & Delete": "Počisti in izbriši",
"Purge all reading data": "Počisti vse podatke o branju",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "To trajno izbriše napredek branja, opombe in zaznamke. Tega ni mogoče razveljaviti.",
"Also erase reading progress, notes, and bookmarks.": "Izbriše tudi napredek branja, opombe in zaznamke."
}
@@ -509,6 +509,7 @@
"Show Remaining Time": "Visa återstående tid",
"Show Remaining Pages": "Visa återstående sidor",
"Show Reading Progress": "Visa läsframsteg",
"Show Progress Bar": "Visa förloppsindikator",
"Reading Progress Style": "Läsframstegsstil",
"Page Number": "Sidnummer",
"Percentage": "Procent",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Automatisk synkronisering",
"Purged book data: {{title}}": "Bokdata rensad: {{title}}",
"Failed to purge book data: {{title}}": "Det gick inte att rensa bokdata: {{title}}",
"Purge Book Data": "Rensa all bokdata",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Detta raderar permanent boken och all dess data, inklusive läsförlopp, anteckningar och bokmärken. Detta kan inte ångras.",
"Purge Data": "Rensa all data",
"Erase the book and all its data, including reading progress": "Raderar boken och all data, inklusive läsförlopp",
"More Actions": "Fler åtgärder",
"Sign in and make the book available to share it": "Logga in och gör boken tillgänglig för att dela den",
"Download the book to export it": "Ladda ner boken för att exportera den",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Det gick inte att spara bilden",
"Share Image": "Dela bild",
"Save Image": "Spara bild",
"Image saved to gallery": "Bilden har sparats i galleriet"
"Image saved to gallery": "Bilden har sparats i galleriet",
"Please select a whole word or uncheck the \"Whole word\" option.": "Markera ett helt ord eller avmarkera alternativet ”Hela ordet”.",
"Regex:": "Regex:",
"Invalid regular expression": "Ogiltigt reguljärt uttryck",
"Find pattern cannot be empty": "Sökmönstret får inte vara tomt",
"Add Rule": "Lägg till regel",
"Find...": "Sök…",
"Replace with...": "Ersätt med…",
"Purge & Delete": "Rensa och ta bort",
"Purge all reading data": "Rensa alla läsdata",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Detta raderar permanent läsframsteg, anteckningar och bokmärken. Detta kan inte ångras.",
"Also erase reading progress, notes, and bookmarks.": "Raderar även läsframsteg, anteckningar och bokmärken."
}
@@ -267,6 +267,7 @@
"Show Remaining Time": "மீதமுள்ள நேரத்தைக் காட்டவும்",
"Show Remaining Pages": "மீதமுள்ள பக்கங்களைக் காட்டவும்",
"Show Reading Progress": "வாசிப்பு முன்னேற்றத்தைக் காட்டவும்",
"Show Progress Bar": "முன்னேற்றப் பட்டியைக் காட்டவும்",
"Reading Progress Style": "வாசிப்பு முன்னேற்ற பாணி",
"Page Number": "பக்க எண்",
"Percentage": "சதவீதம்",
@@ -1758,10 +1759,6 @@
"Auto Sync": "தானியங்கு ஒத்திசைவு",
"Purged book data: {{title}}": "புத்தகத் தரவு அழிக்கப்பட்டது: {{title}}",
"Failed to purge book data: {{title}}": "புத்தகத் தரவை அழிக்க முடியவில்லை: {{title}}",
"Purge Book Data": "புத்தகத்தின் எல்லா தரவையும் அழி",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "இது புத்தகத்தையும் வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புத்தகக்குறிகள் உள்ளிட்ட அதன் அனைத்து தரவையும் நிரந்தரமாக அழிக்கும். இதை மீட்டெடுக்க முடியாது.",
"Purge Data": "எல்லா தரவையும் அழி",
"Erase the book and all its data, including reading progress": "வாசிப்பு முன்னேற்றம் உட்பட புத்தகத்தையும் அதன் எல்லா தரவையும் அழிக்கும்",
"More Actions": "மேலும் செயல்கள்",
"Sign in and make the book available to share it": "பகிர உள்நுழைந்து புத்தகத்தைக் கிடைக்கச் செய்யவும்",
"Download the book to export it": "ஏற்றுமதி செய்ய புத்தகத்தைப் பதிவிறக்கவும்",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "படத்தைச் சேமிக்க முடியவில்லை",
"Share Image": "படத்தைப் பகிர்",
"Save Image": "படத்தைச் சேமி",
"Image saved to gallery": "படம் கேலரியில் சேமிக்கப்பட்டது"
"Image saved to gallery": "படம் கேலரியில் சேமிக்கப்பட்டது",
"Please select a whole word or uncheck the \"Whole word\" option.": "முழுச் சொல்லைத் தேர்ந்தெடுக்கவும் அல்லது \"முழு சொல்\" விருப்பத்தை நீக்கவும்.",
"Regex:": "Regex:",
"Invalid regular expression": "தவறான வழக்கமான வெளிப்பாடு",
"Find pattern cannot be empty": "தேடல் வடிவத்தை வெறுமையாக வைக்க முடியாது",
"Add Rule": "விதியைச் சேர்க்கவும்",
"Find...": "தேடவும்…",
"Replace with...": "மாற்றவும்…",
"Purge & Delete": "அழித்து நீக்கு",
"Purge all reading data": "அனைத்து வாசிப்புத் தரவையும் அழி",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "இது வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளை நிரந்தரமாக அழிக்கும். இதை மீட்டெடுக்க முடியாது.",
"Also erase reading progress, notes, and bookmarks.": "வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளையும் அழிக்கும்."
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "ไม่รองรับ TTS สำหรับเอกสารนี้",
"Reset Password": "รีเซ็ตพาสเวิร์ด",
"Show Reading Progress": "แสดงความคืบหน้าในการอ่าน",
"Show Progress Bar": "แสดงแถบความคืบหน้า",
"Reading Progress Style": "สไตล์ความก้าวหน้าในการอ่าน",
"Page Number": "หมายเลขหน้า",
"Percentage": "เปอร์เซ็นต์",
@@ -1725,10 +1726,6 @@
"Auto Sync": "ซิงค์อัตโนมัติ",
"Purged book data: {{title}}": "ล้างข้อมูลหนังสือแล้ว: {{title}}",
"Failed to purge book data: {{title}}": "ล้างข้อมูลหนังสือไม่สำเร็จ: {{title}}",
"Purge Book Data": "ล้างข้อมูลหนังสือทั้งหมด",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "การดำเนินการนี้จะลบหนังสือและข้อมูลทั้งหมดอย่างถาวร รวมถึงความคืบหน้าการอ่าน บันทึก และที่คั่นหนังสือ ไม่สามารถเลิกทำได้",
"Purge Data": "ล้างข้อมูลทั้งหมด",
"Erase the book and all its data, including reading progress": "ลบหนังสือและข้อมูลทั้งหมด รวมถึงความคืบหน้าการอ่าน",
"More Actions": "การทำงานเพิ่มเติม",
"Sign in and make the book available to share it": "ลงชื่อเข้าใช้และทำให้หนังสือพร้อมใช้งานเพื่อแชร์",
"Download the book to export it": "ดาวน์โหลดหนังสือเพื่อส่งออก",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "ไม่สามารถบันทึกรูปภาพได้",
"Share Image": "แชร์รูปภาพ",
"Save Image": "บันทึกรูปภาพ",
"Image saved to gallery": "บันทึกรูปภาพไปยังแกลเลอรีแล้ว"
"Image saved to gallery": "บันทึกรูปภาพไปยังแกลเลอรีแล้ว",
"Please select a whole word or uncheck the \"Whole word\" option.": "โปรดเลือกทั้งคำ หรือยกเลิกการเลือกตัวเลือก \"คำทั้งคำ\"",
"Regex:": "Regex:",
"Invalid regular expression": "นิพจน์ทั่วไปไม่ถูกต้อง",
"Find pattern cannot be empty": "รูปแบบการค้นหาต้องไม่ว่างเปล่า",
"Add Rule": "เพิ่มกฎ",
"Find...": "ค้นหา…",
"Replace with...": "แทนที่ด้วย…",
"Purge & Delete": "ล้างและลบ",
"Purge all reading data": "ล้างข้อมูลการอ่านทั้งหมด",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "การดำเนินการนี้จะลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กอย่างถาวร ไม่สามารถเลิกทำได้",
"Also erase reading progress, notes, and bookmarks.": "ลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กด้วย"
}
@@ -448,6 +448,7 @@
"TTS not supported for this document": "TTS bu belge için desteklenmiyor",
"Reset Password": "Şifreyi Sıfırla",
"Show Reading Progress": "Okuma İlerlemesini Göster",
"Show Progress Bar": "İlerleme Çubuğunu Göster",
"Reading Progress Style": "Okuma İlerlemesi Stili",
"Page Number": "Sayfa Numarası",
"Percentage": "Yüzde",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Otomatik Senkronizasyon",
"Purged book data: {{title}}": "Kitap verileri silindi: {{title}}",
"Failed to purge book data: {{title}}": "Kitap verileri silinemedi: {{title}}",
"Purge Book Data": "Tüm kitap verilerini sil",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Bu işlem kitabı ve okuma ilerlemesi, notlar ve yer imleri dahil tüm verilerini kalıcı olarak siler. Bu işlem geri alınamaz.",
"Purge Data": "Tüm verileri sil",
"Erase the book and all its data, including reading progress": "Kitabı ve okuma ilerlemesi dahil tüm verilerini siler",
"More Actions": "Diğer eylemler",
"Sign in and make the book available to share it": "Paylaşmak için oturum açın ve kitabı kullanılabilir hale getirin",
"Download the book to export it": "Dışa aktarmak için kitabı indirin",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Görsel kaydedilemedi",
"Share Image": "Görseli Paylaş",
"Save Image": "Görseli Kaydet",
"Image saved to gallery": "Görsel galeriye kaydedildi"
"Image saved to gallery": "Görsel galeriye kaydedildi",
"Please select a whole word or uncheck the \"Whole word\" option.": "Lütfen tam bir kelime seçin veya \"Tam kelime\" seçeneğinin işaretini kaldırın.",
"Regex:": "Regex:",
"Invalid regular expression": "Geçersiz düzenli ifade",
"Find pattern cannot be empty": "Arama deseni boş olamaz",
"Add Rule": "Kural ekle",
"Find...": "Ara…",
"Replace with...": "Şununla değiştir…",
"Purge & Delete": "Temizle ve sil",
"Purge all reading data": "Tüm okuma verilerini temizle",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu, okuma ilerlemesini, notları ve yer işaretlerini kalıcı olarak siler. Bu işlem geri alınamaz.",
"Also erase reading progress, notes, and bookmarks.": "Okuma ilerlemesi, notlar ve yer işaretlerini de siler."
}
@@ -456,6 +456,7 @@
"TTS not supported for this document": "TTS не підтримується в цьому документі",
"Reset Password": "Скинути пароль",
"Show Reading Progress": "Показати проґрес читання",
"Show Progress Bar": "Показати смугу прогресу",
"Reading Progress Style": "Стиль проґресу читання",
"Page Number": "Номер сторінки",
"Percentage": "Відсотки",
@@ -1824,10 +1825,6 @@
"Auto Sync": "Автоматична синхронізація",
"Purged book data: {{title}}": "Дані книги очищено: {{title}}",
"Failed to purge book data: {{title}}": "Не вдалося очистити дані книги: {{title}}",
"Purge Book Data": "Очистити дані книги",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Це назавжди видалить книгу та всі її дані, зокрема прогрес читання, нотатки та закладки. Цю дію неможливо скасувати.",
"Purge Data": "Очистити дані",
"Erase the book and all its data, including reading progress": "Видаляє книгу та всі її дані, зокрема прогрес читання",
"More Actions": "Більше дій",
"Sign in and make the book available to share it": "Увійдіть і зробіть книгу доступною, щоб поділитися нею",
"Download the book to export it": "Завантажте книгу, щоб експортувати її",
@@ -1846,5 +1843,16 @@
"Failed to save the image": "Не вдалося зберегти зображення",
"Share Image": "Поділитися зображенням",
"Save Image": "Зберегти зображення",
"Image saved to gallery": "Зображення збережено в галереї"
"Image saved to gallery": "Зображення збережено в галереї",
"Please select a whole word or uncheck the \"Whole word\" option.": "Виберіть ціле слово або зніміть прапорець «Ціле слово».",
"Regex:": "Regex:",
"Invalid regular expression": "Недійсний регулярний вираз",
"Find pattern cannot be empty": "Шаблон пошуку не може бути порожнім",
"Add Rule": "Додати правило",
"Find...": "Пошук…",
"Replace with...": "Замінити на…",
"Purge & Delete": "Очистити та видалити",
"Purge all reading data": "Очистити всі дані читання",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Це назавжди видалить проґрес читання, нотатки та закладки. Цю дію не можна скасувати.",
"Also erase reading progress, notes, and bookmarks.": "Також видаляє проґрес читання, нотатки та закладки."
}
@@ -1155,6 +1155,7 @@
"Show Remaining Time": "Qolgan vaqtni koʻrsatish",
"Show Remaining Pages": "Qolgan sahifalarni koʻrsatish",
"Show Reading Progress": "Oʻqish jarayonini koʻrsatish",
"Show Progress Bar": "Jarayon panelini koʻrsatish",
"Reading Progress Style": "Oʻqish jarayoni uslubi",
"Percentage": "Foiz",
"Show Current Time": "Joriy vaqtni koʻrsatish",
@@ -1758,10 +1759,6 @@
"Auto Sync": "Avtomatik sinxronlash",
"Purged book data: {{title}}": "Kitob maʼlumotlari oʻchirildi: {{title}}",
"Failed to purge book data: {{title}}": "Kitob maʼlumotlarini oʻchirib boʻlmadi: {{title}}",
"Purge Book Data": "Kitobning barcha maʼlumotlarini oʻchirish",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Bu kitobni va uning barcha maʼlumotlarini, jumladan oʻqish jarayoni, eslatmalar va xatchoplarni butunlay oʻchiradi. Buni qaytarib boʻlmaydi.",
"Purge Data": "Barcha maʼlumotlarni oʻchirish",
"Erase the book and all its data, including reading progress": "Kitobni va uning barcha maʼlumotlarini, jumladan oʻqish jarayonini oʻchiradi",
"More Actions": "Qoʻshimcha amallar",
"Sign in and make the book available to share it": "Ulashish uchun tizimga kiring va kitobni mavjud qiling",
"Download the book to export it": "Eksport qilish uchun kitobni yuklab oling",
@@ -1780,5 +1777,16 @@
"Failed to save the image": "Rasmni saqlab bo'lmadi",
"Share Image": "Rasmni ulashish",
"Save Image": "Rasmni saqlash",
"Image saved to gallery": "Rasm galereyaga saqlandi"
"Image saved to gallery": "Rasm galereyaga saqlandi",
"Please select a whole word or uncheck the \"Whole word\" option.": "Butun soʻzni tanlang yoki \"Butun soʻz\" amalini olib tashlang.",
"Regex:": "Regex:",
"Invalid regular expression": "Yaroqsiz muntazam ifoda",
"Find pattern cannot be empty": "Qidiruv namunasi boʻsh boʻlishi mumkin emas",
"Add Rule": "Qoida qoʻshish",
"Find...": "Qidirish...",
"Replace with...": "Bilan almashtirish...",
"Purge & Delete": "Tozalash va oʻchirish",
"Purge all reading data": "Barcha oʻqish maʼlumotlarini tozalash",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu oʻqish jarayoni, eslatmalar va xatchoʻplarni butunlay oʻchiradi. Buni qaytarib boʻlmaydi.",
"Also erase reading progress, notes, and bookmarks.": "Shuningdek, oʻqish jarayoni, eslatmalar va xatchoʻplarni ham oʻchiradi."
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "TTS không được hỗ trợ cho tài liệu này",
"Reset Password": "Đặt lại mật khẩu",
"Show Reading Progress": "Hiển thị tiến độ đọc",
"Show Progress Bar": "Hiển thị thanh tiến độ",
"Reading Progress Style": "Phong cách tiến độ đọc",
"Page Number": "Số trang",
"Percentage": "Phần trăm",
@@ -1725,10 +1726,6 @@
"Auto Sync": "Tự động đồng bộ",
"Purged book data: {{title}}": "Đã xóa dữ liệu sách: {{title}}",
"Failed to purge book data: {{title}}": "Không thể xóa dữ liệu sách: {{title}}",
"Purge Book Data": "Xóa toàn bộ dữ liệu sách",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Thao tác này sẽ xóa vĩnh viễn sách và toàn bộ dữ liệu của nó, bao gồm tiến độ đọc, ghi chú và dấu trang. Không thể hoàn tác.",
"Purge Data": "Xóa toàn bộ dữ liệu",
"Erase the book and all its data, including reading progress": "Xóa sách và toàn bộ dữ liệu, bao gồm tiến độ đọc",
"More Actions": "Thao tác khác",
"Sign in and make the book available to share it": "Đăng nhập và tải sách về để chia sẻ",
"Download the book to export it": "Tải sách về để xuất ra",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "Không thể lưu hình ảnh",
"Share Image": "Chia sẻ hình ảnh",
"Save Image": "Lưu hình ảnh",
"Image saved to gallery": "Đã lưu hình ảnh vào thư viện"
"Image saved to gallery": "Đã lưu hình ảnh vào thư viện",
"Please select a whole word or uncheck the \"Whole word\" option.": "Vui lòng chọn một từ hoàn chỉnh hoặc bỏ chọn tùy chọn \"Toàn bộ từ\".",
"Regex:": "Regex:",
"Invalid regular expression": "Biểu thức chính quy không hợp lệ",
"Find pattern cannot be empty": "Mẫu tìm kiếm không được để trống",
"Add Rule": "Thêm quy tắc",
"Find...": "Tìm kiếm…",
"Replace with...": "Thay thế bằng…",
"Purge & Delete": "Dọn sạch và xóa",
"Purge all reading data": "Xóa toàn bộ dữ liệu đọc",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Thao tác này xóa vĩnh viễn tiến độ đọc, ghi chú và dấu trang. Không thể hoàn tác.",
"Also erase reading progress, notes, and bookmarks.": "Đồng thời xóa tiến độ đọc, ghi chú và dấu trang."
}
@@ -445,6 +445,7 @@
"TTS not supported for this document": "TTS 不支持此文档",
"Reset Password": "重置密码",
"Show Reading Progress": "显示阅读进度",
"Show Progress Bar": "显示进度条",
"Reading Progress Style": "阅读进度样式",
"Page Number": "页码",
"Percentage": "百分比",
@@ -1725,10 +1726,6 @@
"Auto Sync": "自动同步",
"Purged book data: {{title}}": "已清除书籍数据:{{title}}",
"Failed to purge book data: {{title}}": "清除书籍数据失败:{{title}}",
"Purge Book Data": "清除书籍数据",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "这将永久删除该书籍及其所有数据,包括阅读进度、笔记和书签。此操作无法撤销。",
"Purge Data": "清除数据",
"Erase the book and all its data, including reading progress": "删除该书籍及其所有数据,包括阅读进度",
"More Actions": "更多操作",
"Sign in and make the book available to share it": "登录并使书籍可用后即可分享",
"Download the book to export it": "下载书籍后即可导出",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "图片保存失败",
"Share Image": "分享图片",
"Save Image": "保存图片",
"Image saved to gallery": "图片已保存到相册"
"Image saved to gallery": "图片已保存到相册",
"Please select a whole word or uncheck the \"Whole word\" option.": "请选择完整的单词,或取消勾选\"全词匹配\"选项。",
"Regex:": "正则:",
"Invalid regular expression": "正则表达式无效",
"Find pattern cannot be empty": "查找内容不能为空",
"Add Rule": "添加规则",
"Find...": "查找…",
"Replace with...": "替换为…",
"Purge & Delete": "清除并删除",
"Purge all reading data": "清除所有阅读数据",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "这将永久清除阅读进度、笔记和书签,且无法撤销。",
"Also erase reading progress, notes, and bookmarks.": "同时清除阅读进度、笔记和书签。"
}
@@ -444,6 +444,7 @@
"TTS not supported for this document": "TTS 不支援此文件",
"Reset Password": "重設密碼",
"Show Reading Progress": "顯示閱讀進度",
"Show Progress Bar": "顯示進度列",
"Reading Progress Style": "閱讀進度樣式",
"Page Number": "頁碼",
"Percentage": "百分比",
@@ -1725,10 +1726,6 @@
"Auto Sync": "自動同步",
"Purged book data: {{title}}": "已清除書籍資料:{{title}}",
"Failed to purge book data: {{title}}": "清除書籍資料失敗:{{title}}",
"Purge Book Data": "清除書籍資料",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "這將永久刪除該書籍及其所有資料,包括閱讀進度、筆記和書籤。此操作無法復原。",
"Purge Data": "清除資料",
"Erase the book and all its data, including reading progress": "刪除該書籍及其所有資料,包括閱讀進度",
"More Actions": "更多操作",
"Sign in and make the book available to share it": "登入並使書籍可用後即可分享",
"Download the book to export it": "下載書籍後即可匯出",
@@ -1747,5 +1744,16 @@
"Failed to save the image": "圖片儲存失敗",
"Share Image": "分享圖片",
"Save Image": "儲存圖片",
"Image saved to gallery": "圖片已儲存至相簿"
"Image saved to gallery": "圖片已儲存至相簿",
"Please select a whole word or uncheck the \"Whole word\" option.": "請選取完整的單字,或取消勾選「全詞匹配」選項。",
"Regex:": "正則:",
"Invalid regular expression": "正規表示式無效",
"Find pattern cannot be empty": "尋找內容不可為空",
"Add Rule": "新增規則",
"Find...": "尋找…",
"Replace with...": "取代為…",
"Purge & Delete": "清除並刪除",
"Purge all reading data": "清除所有閱讀資料",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "這將永久清除閱讀進度、筆記和書籤,且無法復原。",
"Also erase reading progress, notes, and bookmarks.": "同時清除閱讀進度、筆記和書籤。"
}
@@ -227,6 +227,54 @@ describe('ProofreadPopup Component', () => {
});
});
describe('Regex Toggle', () => {
it('should render the regex checkbox (off by default)', () => {
renderWithProviders(<ProofreadPopup {...defaultProps} />);
const regexLabel = screen.getByText('Regex:');
expect(regexLabel).toBeTruthy();
const checkbox = regexLabel
.closest('label')!
.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
expect(checkbox).toBeTruthy();
expect(checkbox!.checked).toBe(false);
});
it('should call onConfirm with isRegex true when regex is enabled', async () => {
renderWithProviders(<ProofreadPopup {...defaultProps} />);
const regexLabel = screen.getByText('Regex:');
const regexCheckbox = regexLabel
.closest('label')!
.querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(regexCheckbox);
const input = screen.getByPlaceholderText('Enter text...');
fireEvent.change(input, { target: { value: 'replacement' } });
const applyButton = screen.getByText('Apply');
fireEvent.click(applyButton);
await waitFor(() => {
expect(mockOnConfirm).toHaveBeenCalledWith(expect.objectContaining({ isRegex: true }));
});
});
it('should default isRegex to false when the toggle is left off', async () => {
renderWithProviders(<ProofreadPopup {...defaultProps} />);
const input = screen.getByPlaceholderText('Enter text...');
fireEvent.change(input, { target: { value: 'replacement' } });
const applyButton = screen.getByText('Apply');
fireEvent.click(applyButton);
await waitFor(() => {
expect(mockOnConfirm).toHaveBeenCalledWith(expect.objectContaining({ isRegex: false }));
});
});
});
describe('Click Outside Behavior', () => {
it('should not call onClose when clicking inside the menu', () => {
renderWithProviders(<ProofreadPopup {...defaultProps} />);
@@ -12,6 +12,8 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useProofreadStore } from '@/store/proofreadStore';
import { eventDispatcher } from '@/utils/event';
import { ProofreadRule } from '@/types/book';
// ------------------------------
@@ -97,6 +99,7 @@ describe('ProofreadRulesManager', () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
it('renders book and library (global) proofreading rules from stores', async () => {
@@ -245,6 +248,61 @@ describe('ProofreadRulesManager', () => {
expect(screen.getByText("'book-hit'")).toBeTruthy();
});
it('renders a drag handle for each reorderable rule', async () => {
const selectionRule: ProofreadRule = {
id: 's1',
scope: 'selection',
pattern: 'only-once',
replacement: 'single-hit',
enabled: true,
isRegex: false,
caseSensitive: true,
order: 1,
wholeWord: true,
cfi: 'epubcfi(/6/14!/4/2,/1:0,/1:4)',
sectionHref: 'chapter1.html',
};
const bookRule: ProofreadRule = {
id: 'b1',
scope: 'book',
pattern: 'book-wide',
replacement: 'book-hit',
enabled: true,
isRegex: false,
caseSensitive: true,
order: 2,
wholeWord: true,
};
(useSettingsStore.setState as unknown as (state: unknown) => void)({
settings: { ...DEFAULT_SYSTEM_SETTINGS, globalViewSettings: { proofreadRules: [] } },
});
(useReaderStore.setState as unknown as (state: unknown) => void)({
viewStates: { book1: { viewSettings: { proofreadRules: [selectionRule, bookRule] } } },
});
(useBookDataStore.setState as unknown as (state: unknown) => void)({
booksData: {
book1: {
id: 'book1',
book: null,
file: null,
config: { viewSettings: { proofreadRules: [selectionRule, bookRule] } },
bookDoc: null,
isFixedLayout: false,
},
},
});
useSidebarStore.setState({ sideBarBookKey: 'book1' });
renderWithProviders(<ProofreadRulesManager />);
await Promise.resolve();
setProofreadRulesVisibility(true);
await screen.findByRole('dialog');
const handles = screen.getAllByLabelText('Drag to reorder');
expect(handles.length).toBe(2);
});
it('displays correct scope labels for different rule types', async () => {
const selectionRule: ProofreadRule = {
id: 's1',
@@ -484,6 +542,116 @@ describe('ProofreadRulesManager', () => {
expect(screen.getByText('No book-level replacement rules')).toBeTruthy();
});
describe('Add Rule form', () => {
const openManagerForAdd = async () => {
(useSettingsStore.setState as unknown as (state: unknown) => void)({
settings: {
...DEFAULT_SYSTEM_SETTINGS,
globalViewSettings: { proofreadRules: [] },
},
});
(useReaderStore.setState as unknown as (state: unknown) => void)({
viewStates: { book1: { viewSettings: { proofreadRules: [] } } },
});
useSidebarStore.setState({ sideBarBookKey: 'book1' });
const addRuleSpy = vi
.spyOn(useProofreadStore.getState(), 'addRule')
.mockResolvedValue({} as ProofreadRule);
const recreateSpy = vi
.spyOn(useReaderStore.getState(), 'recreateViewer')
.mockResolvedValue(undefined as never);
renderWithProviders(<ProofreadRulesManager />);
await Promise.resolve();
setProofreadRulesVisibility(true);
await screen.findByRole('dialog');
return { addRuleSpy, recreateSpy };
};
it('creates a book-scoped rule from typed pattern + replacement', async () => {
const { addRuleSpy } = await openManagerForAdd();
fireEvent.change(screen.getByPlaceholderText('Find...'), { target: { value: 'colour' } });
fireEvent.change(screen.getByPlaceholderText('Replace with...'), {
target: { value: 'color' },
});
fireEvent.click(screen.getByRole('button', { name: 'Add Rule' }));
await new Promise((r) => setTimeout(r, 0));
expect(addRuleSpy).toHaveBeenCalledWith(
expect.anything(),
'book1',
expect.objectContaining({
scope: 'book',
pattern: 'colour',
replacement: 'color',
isRegex: false,
}),
);
});
it('creates a regex rule when the Regex toggle is on', async () => {
const { addRuleSpy } = await openManagerForAdd();
fireEvent.change(screen.getByPlaceholderText('Find...'), { target: { value: '\\d+' } });
fireEvent.change(screen.getByPlaceholderText('Replace with...'), {
target: { value: '#' },
});
const regexLabel = screen.getByText('Regex:');
const regexCheckbox = regexLabel
.closest('label')!
.querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(regexCheckbox);
fireEvent.click(screen.getByRole('button', { name: 'Add Rule' }));
await new Promise((r) => setTimeout(r, 0));
expect(addRuleSpy).toHaveBeenCalledWith(
expect.anything(),
'book1',
expect.objectContaining({ pattern: '\\d+', isRegex: true }),
);
});
it('disables the Add Rule button until a find pattern is entered', async () => {
const { addRuleSpy } = await openManagerForAdd();
const button = screen.getByRole('button', { name: 'Add Rule' }) as HTMLButtonElement;
expect(button.disabled).toBe(true);
fireEvent.click(button);
await new Promise((r) => setTimeout(r, 0));
expect(addRuleSpy).not.toHaveBeenCalled();
fireEvent.change(screen.getByPlaceholderText('Find...'), { target: { value: 'colour' } });
expect(button.disabled).toBe(false);
});
it('warns and does not add when the regex is invalid', async () => {
const { addRuleSpy } = await openManagerForAdd();
const dispatchSpy = vi.spyOn(eventDispatcher, 'dispatch');
fireEvent.change(screen.getByPlaceholderText('Find...'), { target: { value: '(' } });
const regexLabel = screen.getByText('Regex:');
const regexCheckbox = regexLabel
.closest('label')!
.querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(regexCheckbox);
fireEvent.click(screen.getByRole('button', { name: 'Add Rule' }));
await new Promise((r) => setTimeout(r, 0));
expect(addRuleSpy).not.toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
'toast',
expect.objectContaining({ type: 'warning' }),
);
});
});
it('merges book and library rules correctly in book section', async () => {
const libraryRule: ProofreadRule = {
id: 'l1',
@@ -146,6 +146,58 @@ describe('SETTINGS_WHITELIST', () => {
test('does NOT sync dictionarySettings.defaultProviderId (per-device last-used tab)', () => {
expect(SETTINGS_WHITELIST).not.toContain('dictionarySettings.defaultProviderId');
});
test('syncs library-scope proofread rules (issue #4700 — PC rules not reaching mobile)', () => {
expect(SETTINGS_WHITELIST).toContain('globalViewSettings.proofreadRules');
});
});
describe('settingsAdapter proofread rules', () => {
test('pack ∘ unpack round-trips globalViewSettings.proofreadRules', () => {
const proofreadRules = [
{
id: 'r1',
scope: 'library',
pattern: 'colour',
replacement: 'color',
enabled: true,
isRegex: false,
caseSensitive: true,
order: 1000,
wholeWord: true,
},
];
const record: SettingsRemoteRecord = {
name: 'singleton',
patch: {
globalViewSettings: { proofreadRules },
} as unknown as Partial<SystemSettings>,
};
const fields = settingsAdapter.pack(record);
expect(fields['globalViewSettings.proofreadRules']).toEqual(proofreadRules);
const out = settingsAdapter.unpack(fields);
expect(out.patch.globalViewSettings?.proofreadRules).toEqual(proofreadRules);
});
test('unpackRow reconstructs proofread rules from a CRDT envelope', () => {
const proofreadRules = [
{
id: 'r1',
scope: 'library',
pattern: 'teh',
replacement: 'the',
enabled: true,
isRegex: false,
caseSensitive: false,
order: 1000,
wholeWord: true,
},
];
const row = makeRow({ 'globalViewSettings.proofreadRules': env(proofreadRules) });
const out = settingsAdapter.unpackRow(row, '');
expect(out).not.toBeNull();
expect(out!.patch.globalViewSettings?.proofreadRules).toEqual(proofreadRules);
});
});
describe('readPath / writePath', () => {
@@ -503,4 +503,87 @@ describe('proofreadStore', () => {
).rejects.toThrow('Rule not found: no-exist');
});
});
// -----------------------------------------------------------------------
// reorderRules
// -----------------------------------------------------------------------
describe('reorderRules', () => {
test('reassigns order fields for book-scope rules by position; saves config only', async () => {
mockViewSettingsMap['book1'] = emptyViewSettings({
proofreadRules: [
makeRule({ id: 'a', scope: 'book', order: 1000 }),
makeRule({ id: 'b', scope: 'book', order: 1000 }),
makeRule({ id: 'c', scope: 'book', order: 1000 }),
],
});
await useProofreadStore.getState().reorderRules(envConfig, 'book1', ['c', 'a', 'b']);
const saved = mockViewSettingsMap['book1']!.proofreadRules!;
const orderById = Object.fromEntries(saved.map((r) => [r.id, r.order]));
expect(orderById).toEqual({ c: 0, a: 1, b: 2 });
expect(mockSaveConfig).toHaveBeenCalledTimes(1);
expect(mockSaveSettings).not.toHaveBeenCalled();
});
test('reassigns order for library-scope rules; saves settings only', async () => {
mockViewSettingsMap['book1'] = emptyViewSettings({ proofreadRules: [] });
mockGlobalViewSettingsHolder.current = emptyViewSettings({
proofreadRules: [
makeRule({ id: 'g1', scope: 'library', order: 1000 }),
makeRule({ id: 'g2', scope: 'library', order: 1000 }),
],
});
await useProofreadStore.getState().reorderRules(envConfig, 'book1', ['g2', 'g1']);
const savedSettings = mockSaveSettings.mock.calls[0]![1] as {
globalViewSettings: ViewSettings;
};
const saved = savedSettings.globalViewSettings.proofreadRules!;
const orderById = Object.fromEntries(saved.map((r) => [r.id, r.order]));
expect(orderById).toEqual({ g2: 0, g1: 1 });
expect(mockSaveSettings).toHaveBeenCalledTimes(1);
expect(mockSaveConfig).not.toHaveBeenCalled();
});
test('a merged book+library reorder updates both stores', async () => {
mockViewSettingsMap['book1'] = emptyViewSettings({
proofreadRules: [makeRule({ id: 'bk', scope: 'book', order: 1000 })],
});
mockGlobalViewSettingsHolder.current = emptyViewSettings({
proofreadRules: [makeRule({ id: 'lib', scope: 'library', order: 1000 })],
});
// Drag the library rule above the book rule.
await useProofreadStore.getState().reorderRules(envConfig, 'book1', ['lib', 'bk']);
const savedBook = mockViewSettingsMap['book1']!.proofreadRules!.find((r) => r.id === 'bk');
const savedSettings = mockSaveSettings.mock.calls[0]![1] as {
globalViewSettings: ViewSettings;
};
const savedLib = savedSettings.globalViewSettings.proofreadRules!.find((r) => r.id === 'lib');
expect(savedLib!.order).toBe(0);
expect(savedBook!.order).toBe(1);
expect(mockSaveConfig).toHaveBeenCalledTimes(1);
expect(mockSaveSettings).toHaveBeenCalledTimes(1);
});
test('leaves rules outside the reordered set untouched', async () => {
mockViewSettingsMap['book1'] = emptyViewSettings({
proofreadRules: [
makeRule({ id: 'sel', scope: 'selection', order: 7 }),
makeRule({ id: 'a', scope: 'book', order: 1000 }),
makeRule({ id: 'b', scope: 'book', order: 1000 }),
],
});
await useProofreadStore.getState().reorderRules(envConfig, 'book1', ['b', 'a']);
const saved = mockViewSettingsMap['book1']!.proofreadRules!;
expect(saved.find((r) => r.id === 'sel')!.order).toBe(7);
expect(saved.find((r) => r.id === 'b')!.order).toBe(0);
expect(saved.find((r) => r.id === 'a')!.order).toBe(1);
});
});
});
@@ -1,16 +1,36 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { RiEditLine, RiDeleteBin7Line } from 'react-icons/ri';
import { MdDragIndicator } from 'react-icons/md';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
type Modifier,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useProofreadStore } from '@/store/proofreadStore';
import { useProofreadStore, validateReplacementRulePattern } from '@/store/proofreadStore';
import { ProofreadRule, ProofreadScope } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import Dialog from '@/components/Dialog';
import { SectionTitle } from '@/components/settings/primitives';
const dialogId = 'proofread_rules_window';
@@ -21,6 +41,29 @@ export const setProofreadRulesVisibility = (visible: boolean) => {
}
};
const byOrder = (a: ProofreadRule, b: ProofreadRule): number => (a.order ?? 0) - (b.order ?? 0);
// Lock drag travel to the vertical axis — the lists are vertical, so any
// horizontal motion just lets the drag preview drift out from under the row.
const restrictToVerticalAxis: Modifier = ({ transform }) => ({ ...transform, x: 0 });
// Clamp the drag preview to the list container so a row can't be dragged out.
const restrictToParentElement: Modifier = ({ containerNodeRect, draggingNodeRect, transform }) => {
if (!draggingNodeRect || !containerNodeRect) return transform;
const value = { ...transform };
if (draggingNodeRect.top + transform.y < containerNodeRect.top) {
value.y = containerNodeRect.top - draggingNodeRect.top;
} else if (
draggingNodeRect.bottom + transform.y >
containerNodeRect.top + containerNodeRect.height
) {
value.y = containerNodeRect.top + containerNodeRect.height - draggingNodeRect.bottom;
}
return value;
};
const dragModifiers: Modifier[] = [restrictToVerticalAxis, restrictToParentElement];
const RuleItem: React.FC<{
rule: ProofreadRule;
scope: ProofreadScope;
@@ -145,6 +188,53 @@ const RuleItem: React.FC<{
);
};
// Sortable wrapper: the draggable `<li>` chassis + a left drag handle around
// the existing RuleItem. The handle is the only drag-listener surface so the
// edit/delete buttons stay clickable; it's hidden while the row is in edit mode.
const SortableRuleItem: React.FC<React.ComponentProps<typeof RuleItem>> = (props) => {
const _ = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.rule.id,
});
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
return (
<li
ref={setNodeRef}
style={style}
className={clsx(
'card eink-bordered border-base-200 bg-base-100 border transition-colors',
isDragging ? 'z-10 shadow-md' : 'hover:border-base-300',
)}
>
<div className='flex items-stretch'>
{!props.isEditing && (
<button
type='button'
className={clsx(
'touch-target text-base-content/35 hover:text-base-content/70 flex w-7 shrink-0',
'cursor-grab touch-none items-center justify-center active:cursor-grabbing',
)}
aria-label={_('Drag to reorder')}
title={_('Drag to reorder')}
{...attributes}
{...listeners}
>
<MdDragIndicator className='h-4 w-4' />
</button>
)}
<div className='min-w-0 flex-1'>
<RuleItem {...props} />
</div>
</div>
</li>
);
};
// Hook to manage rules logic
const useReplacementRules = (bookKey: string | null) => {
const { settings } = useSettingsStore();
@@ -159,7 +249,9 @@ const useReplacementRules = (bookKey: string | null) => {
// Prefer persisted rules; fall back to in-memory
const bookRuleSource = persistedBookRules.length ? persistedBookRules : inMemoryRules;
const singleRules = bookRuleSource.filter((r: ProofreadRule) => r.scope === 'selection');
const singleRules = bookRuleSource
.filter((r: ProofreadRule) => r.scope === 'selection')
.sort(byOrder);
const bookScopedRules = bookRuleSource.filter((r: ProofreadRule) => r.scope === 'book');
const globalRules = settings?.globalViewSettings?.proofreadRules || [];
@@ -171,11 +263,16 @@ const useReplacementRules = (bookKey: string | null) => {
(br: ProofreadRule) => br.enabled !== false || globalRuleIds.has(br.id),
);
const mergedBookRules = validBookRules.concat(
globalRules.filter(
(gr: ProofreadRule) => !validBookRules.find((br: ProofreadRule) => br.id === gr.id),
),
);
// Sort by `order` so a drag-to-reorder (which rewrites the order field)
// persists visually; the stable sort keeps insertion order while every
// rule still shares the default order.
const mergedBookRules = validBookRules
.concat(
globalRules.filter(
(gr: ProofreadRule) => !validBookRules.find((br: ProofreadRule) => br.id === gr.id),
),
)
.sort(byOrder);
return { singleRules, bookRules: mergedBookRules };
};
@@ -185,9 +282,23 @@ export const ProofreadRulesManager: React.FC = () => {
const { envConfig } = useEnv();
const { recreateViewer } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { updateRule, removeRule } = useProofreadStore();
const { addRule, updateRule, removeRule, reorderRules } = useProofreadStore();
// dnd-kit sensors mirror the dictionaries reorder list: a small pointer
// distance gate avoids hijacking handle clicks, a touch delay gives mobile
// long-press-to-drag, and the keyboard sensor makes reordering accessible.
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
const [isOpen, setIsOpen] = useState(false);
const [addPattern, setAddPattern] = useState('');
const [addReplacement, setAddReplacement] = useState('');
const [addScope, setAddScope] = useState<Exclude<ProofreadScope, 'selection'>>('book');
const [addIsRegex, setAddIsRegex] = useState(false);
const [addCaseSensitive, setAddCaseSensitive] = useState(true);
const [editing, setEditing] = useState<{
id: string | null;
scope: ProofreadScope | null;
@@ -255,42 +366,89 @@ export const ProofreadRulesManager: React.FC = () => {
}
};
const handleAddRule = async () => {
if (!sideBarBookKey) return;
const pattern = addPattern.trim();
const validation = validateReplacementRulePattern(pattern, addIsRegex);
if (!validation.valid) {
eventDispatcher.dispatch('toast', {
type: 'warning',
message: pattern ? _('Invalid regular expression') : _('Find pattern cannot be empty'),
timeout: 3000,
});
return;
}
await addRule(envConfig, sideBarBookKey, {
scope: addScope,
pattern,
replacement: addReplacement.trim(),
isRegex: addIsRegex,
caseSensitive: addCaseSensitive,
enabled: true,
});
setAddPattern('');
setAddReplacement('');
setAddIsRegex(false);
recreateViewer(envConfig, sideBarBookKey);
};
const handleDragEnd = async (event: DragEndEvent, list: ProofreadRule[]) => {
const { active, over } = event;
if (!over || active.id === over.id || !sideBarBookKey) return;
const ids = list.map((r) => r.id);
const fromIdx = ids.indexOf(String(active.id));
const toIdx = ids.indexOf(String(over.id));
if (fromIdx < 0 || toIdx < 0) return;
const [moved] = ids.splice(fromIdx, 1);
if (!moved) return;
ids.splice(toIdx, 0, moved);
await reorderRules(envConfig, sideBarBookKey, ids);
recreateViewer(envConfig, sideBarBookKey);
};
const renderRuleList = (
rules: ProofreadRule[],
scopeType: ProofreadScope,
title: string,
emptyMessage: string,
) => (
<div className='flex flex-col gap-3'>
<h3 className='text-base-content/90 text-sm font-semibold'>{title}</h3>
<div className='flex flex-col gap-2'>
<SectionTitle>{title}</SectionTitle>
{rules.length === 0 ? (
<div className='border-base-content/10 bg-base-200/30 rounded-lg border border-dashed p-6 text-center'>
<div className='border-base-300 bg-base-200/30 rounded-xl border border-dashed p-6 text-center'>
<p className='text-base-content/50 text-sm'>{emptyMessage}</p>
</div>
) : (
<ul className='flex flex-col gap-2'>
{rules.map((rule) => (
<li
key={rule.id}
className='border-base-content/10 bg-base-100 hover:border-base-content/20 rounded-lg border transition-colors'
>
<RuleItem
rule={rule}
scope={scopeType === 'selection' ? 'selection' : rule.scope}
isEditing={
editing.id === rule.id &&
editing.scope === (scopeType === 'selection' ? 'selection' : rule.scope)
}
editingData={editing}
onEdit={() => startEdit(rule)}
onDelete={() => deleteRule(rule)}
onSave={saveEdit}
onCancel={cancelEdit}
onEditChange={(_, value) => setEditing({ ...editing, replacement: value })}
/>
</li>
))}
</ul>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={dragModifiers}
onDragEnd={(event) => handleDragEnd(event, rules)}
>
<SortableContext items={rules.map((r) => r.id)} strategy={verticalListSortingStrategy}>
<ul className='flex flex-col gap-2'>
{rules.map((rule) => (
<SortableRuleItem
key={rule.id}
rule={rule}
scope={scopeType === 'selection' ? 'selection' : rule.scope}
isEditing={
editing.id === rule.id &&
editing.scope === (scopeType === 'selection' ? 'selection' : rule.scope)
}
editingData={editing}
onEdit={() => startEdit(rule)}
onDelete={() => deleteRule(rule)}
onSave={saveEdit}
onCancel={cancelEdit}
onEditChange={(_, value) => setEditing({ ...editing, replacement: value })}
/>
))}
</ul>
</SortableContext>
</DndContext>
)}
</div>
);
@@ -301,10 +459,81 @@ export const ProofreadRulesManager: React.FC = () => {
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title={_('Proofread Replacement Rules')}
boxClassName='sm:!min-w-[560px] sm:!max-w-[640px] sm:h-auto'
// Cap the height on desktop (where the modal is auto-height) so the body
// scrolls; on mobile the modal is full-height and the body fills it.
boxClassName='sm:!min-w-[560px] sm:!max-w-[640px] sm:h-auto sm:!max-h-[80vh]'
// Drop the body's default horizontal padding so the scrollbar rides the
// modal's right edge (the inner `p-4 sm:p-6` keeps content off it), and
// `min-h-0` lets the flex-grow body shrink-to-scroll inside the capped
// modal instead of overflowing.
contentClassName='!px-0 min-h-0'
>
{isOpen && (
<div className='flex max-h-[70vh] flex-col gap-6 p-4 sm:p-6'>
<div className='flex flex-col gap-6 p-4 sm:p-6'>
<div className='flex flex-col gap-2'>
<SectionTitle>{_('Add Rule')}</SectionTitle>
<div className='card eink-bordered border-base-200 bg-base-100 gap-3 border p-4'>
<input
className='input input-bordered eink-bordered h-11 w-full text-sm focus:outline-none'
placeholder={_('Find...')}
spellCheck='false'
value={addPattern}
onChange={(e) => setAddPattern(e.target.value)}
/>
<input
className='input input-bordered eink-bordered h-11 w-full text-sm focus:outline-none'
placeholder={_('Replace with...')}
spellCheck='false'
value={addReplacement}
onChange={(e) => setAddReplacement(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleAddRule();
}}
/>
<div className='flex flex-wrap items-center gap-x-5 gap-y-3 pt-0.5'>
<label className='flex items-center gap-2'>
<span className='text-base-content/70 text-sm'>{_('Scope:')}</span>
<select
className='select select-sm select-bordered eink-bordered min-h-9 h-9'
value={addScope}
onChange={(e) =>
setAddScope(e.target.value as Exclude<ProofreadScope, 'selection'>)
}
>
<option value='book'>{_('Book')}</option>
<option value='library'>{_('Library')}</option>
</select>
</label>
<label className='flex cursor-pointer items-center gap-2'>
<span className='text-base-content/70 text-sm'>{_('Regex:')}</span>
<input
type='checkbox'
className='toggle toggle-sm'
checked={addIsRegex}
onChange={(e) => setAddIsRegex(e.target.checked)}
/>
</label>
<label className='flex cursor-pointer items-center gap-2'>
<span className='text-base-content/70 text-sm'>{_('Case sensitive:')}</span>
<input
type='checkbox'
className='toggle toggle-sm'
checked={addCaseSensitive}
onChange={(e) => setAddCaseSensitive(e.target.checked)}
/>
</label>
</div>
<div className='border-base-200 mt-1 flex justify-end border-t pt-3'>
<button
className='btn btn-contrast h-10 min-h-10 rounded-lg px-5 text-sm font-medium disabled:opacity-40'
onClick={handleAddRule}
disabled={!addPattern.trim()}
>
{_('Add Rule')}
</button>
</div>
</div>
</div>
{renderRuleList(
singleRules,
'selection',
@@ -1259,7 +1259,12 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
};
const handleProofread = () => {
if (!selection || !selection.text) return;
// With no active selection the shortcut (Ctrl/Cmd+P) has nothing to turn
// into a rule, so reuse it to open the replacement-rules manager instead.
if (!selection || !selection.text) {
setProofreadRulesVisibility(true);
return;
}
setShowAnnotPopup(false);
setShowProofreadPopup(true);
@@ -45,6 +45,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
const [replacementText, setReplacementText] = useState('');
const [caseSensitive, setCaseSensitive] = useState(true);
const [wholeWord, setWholeWord] = useState(!isPunctuationOnly(selection?.text || ''));
const [isRegex, setIsRegex] = useState(false);
const [scope, setScope] = useState<ProofreadScope>('selection');
const [onlyForTTS, setOnlyForTTS] = useState(false);
@@ -66,12 +67,14 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
const range = selection?.range;
if (range) {
// A regex pattern defines its own boundaries, so the whole-word
// validation (which inspects the literal selection) doesn't apply.
const isValidWholeWord = isWholeWord(range, selection?.text || '');
if (wholeWord && !isValidWholeWord) {
if (!isRegex && wholeWord && !isValidWholeWord) {
eventDispatcher.dispatch('toast', {
type: 'warning',
message: 'Please select a whole word or uncheck the "Whole word" option.',
message: _('Please select a whole word or uncheck the "Whole word" option.'),
timeout: 5000,
});
return;
@@ -89,10 +92,10 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
replacement: replacementText.trim(),
cfi: selection.cfi,
sectionHref: progress?.sectionHref,
isRegex: false,
isRegex,
enabled: true,
caseSensitive,
wholeWord: wholeWord,
wholeWord: isRegex ? false : wholeWord,
onlyForTTS: scope !== 'selection' ? onlyForTTS : undefined,
};
onConfirm?.(options);
@@ -196,10 +199,33 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
/>
</label>
<label className='flex cursor-pointer items-center gap-2'>
<label
className={clsx(
'flex items-center gap-2',
isRegex ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
)}
>
<span className='line-clamp-1 text-xs' title={_('Whole word:')}>
{_('Whole word:')}
</span>
<input
type='checkbox'
disabled={isRegex}
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
style={
{
'--tglbg': '#4B5563',
} as React.CSSProperties
}
checked={isRegex ? false : wholeWord}
onChange={(e) => setWholeWord(e.target.checked)}
/>
</label>
<label className='flex cursor-pointer items-center gap-2'>
<span className='line-clamp-1 text-xs' title={_('Regex:')}>
{_('Regex:')}
</span>
<input
type='checkbox'
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
@@ -208,8 +234,8 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
'--tglbg': '#4B5563',
} as React.CSSProperties
}
checked={wholeWord}
onChange={(e) => setWholeWord(e.target.checked)}
checked={isRegex}
onChange={(e) => setIsRegex(e.target.checked)}
/>
</label>
@@ -33,6 +33,10 @@ export const SETTINGS_REPLICA_ID = 'singleton';
export const SETTINGS_WHITELIST = [
'globalViewSettings.userStylesheet',
'globalViewSettings.userUIStylesheet',
// Library-scope proofread (find/replace) rules. Whole-field LWW like the
// other arrays here. Book- and selection-scope rules already ride along the
// book config sync; only these global rules were stranded on one device.
'globalViewSettings.proofreadRules',
'globalReadSettings.customThemes',
'globalReadSettings.customHighlightColors',
'globalReadSettings.userHighlightColors',
@@ -44,6 +44,14 @@ interface ProofreadStoreState {
scope: ProofreadScope,
) => Promise<void>;
toggleRule: (envConfig: EnvConfigType, bookKey: string, ruleId: string) => Promise<void>;
/**
* Persist a drag-to-reorder of one rule category. `orderedIds` is the new
* visual order of the category's rules; each matching rule's `order` field
* is set to its index. A category can span both stores (book + library), so
* book/selection rules are written to the book config and library rules to
* the global settings only the store(s) actually touched are saved.
*/
reorderRules: (envConfig: EnvConfigType, bookKey: string, orderedIds: string[]) => Promise<void>;
}
function createProofreadRule(opts: CreateProofreadRuleOptions): ProofreadRule {
@@ -135,6 +143,36 @@ export const useProofreadStore = create<ProofreadStoreState>(() => ({
const { updateRule } = useProofreadStore.getState();
await updateRule(envConfig, bookKey, ruleId, { enabled: !rule.enabled });
},
reorderRules: async (envConfig, bookKey, orderedIds) => {
const orderById = new Map(orderedIds.map((id, index) => [id, index] as const));
const { getViewSettings } = useReaderStore.getState();
const viewSettings = getViewSettings(bookKey);
if (viewSettings) {
let bookTouched = false;
const updated = (viewSettings.proofreadRules || []).map((r) => {
const order = orderById.get(r.id);
if (order === undefined) return r;
bookTouched = true;
return { ...r, order };
});
if (bookTouched) await updateBookViewSettings(envConfig, bookKey, updated);
}
const { settings } = useSettingsStore.getState();
const globalRules = settings.globalViewSettings?.proofreadRules;
if (globalRules?.length) {
let globalTouched = false;
const updated = globalRules.map((r) => {
const order = orderById.get(r.id);
if (order === undefined) return r;
globalTouched = true;
return { ...r, order };
});
if (globalTouched) await updateGlobalSettings(envConfig, updated);
}
},
}));
async function addBookRule(