This commit is contained in:
@@ -306,7 +306,6 @@
|
||||
"File Size:": "حجم الملف:",
|
||||
"Auto Detect": "الكشف التلقائي",
|
||||
"Touch": "اللمس",
|
||||
"Scrolling Overlap (px)": "تداخل التمرير (بكسل)",
|
||||
"Next Section": "القسم التالي",
|
||||
"Previous Section": "القسم السابق",
|
||||
"Next Page": "الصفحة التالية",
|
||||
@@ -327,5 +326,12 @@
|
||||
"Azure Translator": "مترجم أزور",
|
||||
"Invert Image In Dark Mode": "عكس الصورة في الوضع المظلم",
|
||||
"Help improve Readest": "ساعد في تحسين ريديست",
|
||||
"Sharing anonymized statistics": "مشاركة إحصائيات مجهولة الهوية"
|
||||
"Sharing anonymized statistics": "مشاركة إحصائيات مجهولة الهوية",
|
||||
"Interface Language": "لغة الواجهة",
|
||||
"Translation": "الترجمة",
|
||||
"Enable Translation": "تمكين الترجمة",
|
||||
"Translation Service": "خدمة الترجمة",
|
||||
"Translate To": "ترجمة إلى",
|
||||
"Disable Translation": "تعطيل الترجمة",
|
||||
"Overlap Margin": "هامش التداخل"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Dateigröße:",
|
||||
"Auto Detect": "Automatisch erkennen",
|
||||
"Touch": "Berührung",
|
||||
"Scrolling Overlap (px)": "Scroll-Überlappung (px)",
|
||||
"Next Section": "Nächster Abschnitt",
|
||||
"Previous Section": "Vorheriger Abschnitt",
|
||||
"Next Page": "Nächste Seite",
|
||||
@@ -323,5 +322,12 @@
|
||||
"Azure Translator": "Azure Übersetzer",
|
||||
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln",
|
||||
"Help improve Readest": "Readest verbessern helfen",
|
||||
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen"
|
||||
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen",
|
||||
"Interface Language": "Benutzeroberflächensprache",
|
||||
"Translation": "Übersetzung",
|
||||
"Enable Translation": "Übersetzung aktivieren",
|
||||
"Translation Service": "Übersetzungsdienst",
|
||||
"Translate To": "Übersetzen nach",
|
||||
"Disable Translation": "Übersetzung deaktivieren",
|
||||
"Overlap Margin": "Überlappungsrand"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Μέγεθος αρχείου:",
|
||||
"Auto Detect": "Αυτόματη ανίχνευση",
|
||||
"Touch": "Αφή",
|
||||
"Scrolling Overlap (px)": "Επικάλυψη κύλισης (px)",
|
||||
"Next Section": "Επόμενη ενότητα",
|
||||
"Previous Section": "Προηγούμενη ενότητα",
|
||||
"Next Page": "Επόμενη σελίδα",
|
||||
@@ -323,5 +322,12 @@
|
||||
"Azure Translator": "Μεταφραστής Azure",
|
||||
"Invert Image In Dark Mode": "Αντιστροφή σε σκούρο",
|
||||
"Help improve Readest": "Βοηθήστε στη βελτίωση του Readest",
|
||||
"Sharing anonymized statistics": "Μοιράζομαι ανώνυμες στατιστικές"
|
||||
"Sharing anonymized statistics": "Μοιράζομαι ανώνυμες στατιστικές",
|
||||
"Interface Language": "Γλώσσα διεπαφής",
|
||||
"Translation": "Μετάφραση",
|
||||
"Enable Translation": "Ενεργοποίηση μετάφρασης",
|
||||
"Translation Service": "Υπηρεσία μετάφρασης",
|
||||
"Translate To": "Μετάφραση σε",
|
||||
"Disable Translation": "Απενεργοποίηση μετάφρασης",
|
||||
"Overlap Margin": "Περιθώριο επικάλυψης"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Peso del archivo:",
|
||||
"Auto Detect": "Detección automática",
|
||||
"Touch": "Toque",
|
||||
"Scrolling Overlap (px)": "Superposición de desplazamiento (px)",
|
||||
"Next Section": "Siguiente sección",
|
||||
"Previous Section": "Sección anterior",
|
||||
"Next Page": "Página siguiente",
|
||||
@@ -324,5 +323,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Invertir imagen en oscuro",
|
||||
"Help improve Readest": "Ayuda a mejorar Readest",
|
||||
"Sharing anonymized statistics": "Compartiendo estadísticas anónimas"
|
||||
"Sharing anonymized statistics": "Compartiendo estadísticas anónimas",
|
||||
"Interface Language": "Idioma de la interfaz",
|
||||
"Translation": "Traducción",
|
||||
"Enable Translation": "Habilitar traducción",
|
||||
"Translation Service": "Servicio de traducción",
|
||||
"Translate To": "Traducir a",
|
||||
"Disable Translation": "Deshabilitar traducción",
|
||||
"Overlap Margin": "Margen de superposición"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Taille du fichier :",
|
||||
"Auto Detect": "Détection automatique",
|
||||
"Touch": "Touch",
|
||||
"Scrolling Overlap (px)": "Chevauchement de défilement (px)",
|
||||
"Next Section": "Section suivante",
|
||||
"Previous Section": "Section précédente",
|
||||
"Next Page": "Page suivante",
|
||||
@@ -324,5 +323,12 @@
|
||||
"Azure Translator": "Traducteur Azure",
|
||||
"Invert Image In Dark Mode": "Inverser l'image en mode sombre",
|
||||
"Help improve Readest": "Contribuer à améliorer Readest",
|
||||
"Sharing anonymized statistics": "Partage de statistiques anonymisées"
|
||||
"Sharing anonymized statistics": "Partage de statistiques anonymisées",
|
||||
"Interface Language": "Langue de l'interface",
|
||||
"Translation": "Traduction",
|
||||
"Enable Translation": "Activer la traduction",
|
||||
"Translation Service": "Service de traduction",
|
||||
"Translate To": "Traduire vers",
|
||||
"Disable Translation": " désactiver la traduction",
|
||||
"Overlap Margin": "Marges de chevauchement"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "फ़ाइल आकार:",
|
||||
"Auto Detect": "स्वचालित पहचान",
|
||||
"Touch": "स्पर्श",
|
||||
"Scrolling Overlap (px)": "स्क्रॉलिंग ओवरलैप (px)",
|
||||
"Next Section": "अगला अनुभाग",
|
||||
"Previous Section": "पिछला अनुभाग",
|
||||
"Next Page": "अगला पृष्ठ",
|
||||
@@ -323,5 +322,12 @@
|
||||
"Azure Translator": "एज़्योर अनुवादक",
|
||||
"Invert Image In Dark Mode": "अंधेरे में छवि को उलटें",
|
||||
"Help improve Readest": "Readest में सुधार करने में मदद करें",
|
||||
"Sharing anonymized statistics": "सांख्यिकी साझा करना"
|
||||
"Sharing anonymized statistics": "सांख्यिकी साझा करना",
|
||||
"Interface Language": "इंटरफ़ेस भाषा",
|
||||
"Translation": "अनुवाद",
|
||||
"Enable Translation": "अनुवाद सक्षम करें",
|
||||
"Translation Service": "अनुवाद सेवा",
|
||||
"Translate To": "अनुवाद करें",
|
||||
"Disable Translation": "अनुवाद अक्षम करें",
|
||||
"Overlap Margin": "ओवरलैप मार्जिन"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Ukuran File:",
|
||||
"Auto Detect": "Deteksi Otomatis",
|
||||
"Touch": "Sentuh",
|
||||
"Scrolling Overlap (px)": "Overlapping Gulir (px)",
|
||||
"Next Section": "Bagian Selanjutnya",
|
||||
"Previous Section": "Bagian Sebelumnya",
|
||||
"Next Page": "Halaman Berikutnya",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Inversi Gambar di Gelap",
|
||||
"Help improve Readest": "Bantu tingkatkan Readest",
|
||||
"Sharing anonymized statistics": "Berbagi statistik anonim"
|
||||
"Sharing anonymized statistics": "Berbagi statistik anonim",
|
||||
"Interface Language": "Bahasa Antarmuka",
|
||||
"Translation": "Terjemahan",
|
||||
"Enable Translation": "Aktifkan Terjemahan",
|
||||
"Translation Service": "Layanan Terjemahan",
|
||||
"Translate To": "Terjemahkan Ke",
|
||||
"Disable Translation": "Nonaktifkan Terjemahan",
|
||||
"Overlap Margin": "Margin Tumpang Tindih"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Dimensione file:",
|
||||
"Auto Detect": "Rilevamento automatico",
|
||||
"Touch": "Touch",
|
||||
"Scrolling Overlap (px)": "Overlap scorrimento (px)",
|
||||
"Next Section": "Sezione successiva",
|
||||
"Previous Section": "Sezione precedente",
|
||||
"Next Page": "Pagina successiva",
|
||||
@@ -324,5 +323,12 @@
|
||||
"Azure Translator": "Traduttore Azure",
|
||||
"Invert Image In Dark Mode": "Inverti immagine in scuro",
|
||||
"Help improve Readest": "Aiuta a migliorare Readest",
|
||||
"Sharing anonymized statistics": "Condivisione di statistiche anonime"
|
||||
"Sharing anonymized statistics": "Condivisione di statistiche anonime",
|
||||
"Interface Language": "Lingua dell'interfaccia",
|
||||
"Translation": "Traduzione",
|
||||
"Enable Translation": "Abilita traduzione",
|
||||
"Translation Service": "Servizio di traduzione",
|
||||
"Translate To": "Traduci in",
|
||||
"Disable Translation": "Disabilita traduzione",
|
||||
"Overlap Margin": "Margine di sovrapposizione"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "ファイルサイズ:",
|
||||
"Auto Detect": "自動検出",
|
||||
"Touch": "タッチ",
|
||||
"Scrolling Overlap (px)": "スクロール重複(px)",
|
||||
"Next Section": "次のセクション",
|
||||
"Previous Section": "前のセクション",
|
||||
"Next Page": "次のページ",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure翻訳",
|
||||
"Invert Image In Dark Mode": "ダークモードで画像を反転",
|
||||
"Help improve Readest": "Readestを改善する手助け",
|
||||
"Sharing anonymized statistics": "匿名化された統計情報を共有"
|
||||
"Sharing anonymized statistics": "匿名化された統計情報を共有",
|
||||
"Interface Language": "インターフェースの言語",
|
||||
"Translation": "翻訳",
|
||||
"Enable Translation": "翻訳を有効にする",
|
||||
"Translation Service": "翻訳サービス",
|
||||
"Translate To": "翻訳先",
|
||||
"Disable Translation": "翻訳を無効にする",
|
||||
"Overlap Margin": "オーバーラップマージン"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "파일 크기:",
|
||||
"Auto Detect": "자동 감지",
|
||||
"Touch": "터치",
|
||||
"Scrolling Overlap (px)": "스크롤 겹침 (px)",
|
||||
"Next Section": "다음 섹션",
|
||||
"Previous Section": "이전 섹션",
|
||||
"Next Page": "다음 페이지",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure 번역기",
|
||||
"Invert Image In Dark Mode": "어두운 곳에서 이미지 반전",
|
||||
"Help improve Readest": "Readest 개선에 도움을 주세요",
|
||||
"Sharing anonymized statistics": "익명화된 통계 공유"
|
||||
"Sharing anonymized statistics": "익명화된 통계 공유",
|
||||
"Interface Language": "인터페이스 언어",
|
||||
"Translation": "번역",
|
||||
"Enable Translation": "번역 활성화",
|
||||
"Translation Service": "번역 서비스",
|
||||
"Translate To": "번역 대상 언어",
|
||||
"Disable Translation": "번역 비활성화",
|
||||
"Overlap Margin": "겹침 여백"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"Don’t have an account? Sign up": "Nog geen account? Registreer u",
|
||||
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "De hele wereld is een toneel,\nEn alle mannen en vrouwen slechts spelers;\nZij hebben hun uitgangen en hun entrees,\nEn één man speelt in zijn tijd vele rollen,\nZijn daden zijnde zeven leeftijden.\n\n— William Shakespeare",
|
||||
"Touch": "Raak",
|
||||
"Scrolling Overlap (px)": "Scroll-overlap (px)",
|
||||
"Next Section": "Volgende sectie",
|
||||
"Previous Section": "Vorige sectie",
|
||||
"Next Page": "Volgende pagina",
|
||||
@@ -323,5 +322,12 @@
|
||||
"Azure Translator": "Azure Vertaler",
|
||||
"Invert Image In Dark Mode": "Inverteer afbeelding in het donker",
|
||||
"Help improve Readest": "Help Readest verbeteren",
|
||||
"Sharing anonymized statistics": "Anonieme statistieken delen"
|
||||
"Sharing anonymized statistics": "Anonieme statistieken delen",
|
||||
"Interface Language": "Interface-taal",
|
||||
"Translation": "Vertaling",
|
||||
"Enable Translation": "Vertaling inschakelen",
|
||||
"Translation Service": "Vertaalservice",
|
||||
"Translate To": "Vertalen naar",
|
||||
"Disable Translation": "Vertaling uitschakelen",
|
||||
"Overlap Margin": "Overlap marge"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Rozmiar pliku:",
|
||||
"Auto Detect": "Automatyczne wykrywanie",
|
||||
"Touch": "Dotyk",
|
||||
"Scrolling Overlap (px)": "Nakładanie przewijania (px)",
|
||||
"Next Section": "Następna sekcja",
|
||||
"Previous Section": "Poprzednia sekcja",
|
||||
"Next Page": "Następna strona",
|
||||
@@ -325,5 +324,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Odwróć obraz w ciemności",
|
||||
"Help improve Readest": "Pomóż ulepszyć Readest",
|
||||
"Sharing anonymized statistics": "Udostępnianie zanonimizowanych statystyk"
|
||||
"Sharing anonymized statistics": "Udostępnianie zanonimizowanych statystyk",
|
||||
"Interface Language": "Język interfejsu",
|
||||
"Translation": "Tłumaczenie",
|
||||
"Enable Translation": "Włącz tłumaczenie",
|
||||
"Translation Service": "Usługa tłumaczenia",
|
||||
"Translate To": "Przetłumacz na",
|
||||
"Disable Translation": "Wyłącz tłumaczenie",
|
||||
"Overlap Margin": "Margines nakładania"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Tamanho do arquivo:",
|
||||
"Auto Detect": "Detectar Automaticamente",
|
||||
"Touch": "Toque",
|
||||
"Scrolling Overlap (px)": "Sobreposição de Rolagem (px)",
|
||||
"Next Section": "Próxima seção",
|
||||
"Previous Section": "Seção anterior",
|
||||
"Next Page": "Próxima página",
|
||||
@@ -324,5 +323,12 @@
|
||||
"Azure Translator": "Tradutor Azure",
|
||||
"Invert Image In Dark Mode": "Inverter Imagem no Escuro",
|
||||
"Help improve Readest": "Ajude a melhorar o Readest",
|
||||
"Sharing anonymized statistics": "Compartilhando estatísticas anonimizadas"
|
||||
"Sharing anonymized statistics": "Compartilhando estatísticas anonimizadas",
|
||||
"Interface Language": "Idioma da interface",
|
||||
"Translation": "Tradução",
|
||||
"Enable Translation": "Ativar tradução",
|
||||
"Translation Service": "Serviço de tradução",
|
||||
"Translate To": "Traduzir para",
|
||||
"Disable Translation": "Desativar tradução",
|
||||
"Overlap Margin": "Margem de Sobreposição"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Размер файла:",
|
||||
"Auto Detect": "Автоопределение",
|
||||
"Touch": "Сенсорный экран",
|
||||
"Scrolling Overlap (px)": "Перекрытие прокрутки (px)",
|
||||
"Next Section": "Следующий раздел",
|
||||
"Previous Section": "Предыдущий раздел",
|
||||
"Next Page": "Следующая страница",
|
||||
@@ -325,5 +324,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Инвертировать в тёмной теме",
|
||||
"Help improve Readest": "Помогите улучшить Readest",
|
||||
"Sharing anonymized statistics": "Отправка анонимной статистики"
|
||||
"Sharing anonymized statistics": "Отправка анонимной статистики",
|
||||
"Interface Language": "Язык интерфейса",
|
||||
"Translation": "Перевод",
|
||||
"Enable Translation": "Включить перевод",
|
||||
"Translation Service": "Сервис перевода",
|
||||
"Translate To": "Перевести на",
|
||||
"Disable Translation": "Отключить перевод",
|
||||
"Overlap Margin": "Поле перекрытия"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Dosya Boyutu:",
|
||||
"Auto Detect": "Algıla",
|
||||
"Touch": "Dokunmatik",
|
||||
"Scrolling Overlap (px)": "Kaydırma Üst Üste Binen (px)",
|
||||
"Next Section": "Sonraki Bölüm",
|
||||
"Previous Section": "Önceki Bölüm",
|
||||
"Next Page": "Sonraki Sayfa",
|
||||
@@ -323,5 +322,12 @@
|
||||
"Azure Translator": "Azure Çevirmen",
|
||||
"Invert Image In Dark Mode": "Karartılmış Görüntüyü Ters Çevir",
|
||||
"Help improve Readest": "Readest’i geliştirin",
|
||||
"Sharing anonymized statistics": "Anonim verileri paylaş"
|
||||
"Sharing anonymized statistics": "Anonim verileri paylaş",
|
||||
"Interface Language": "Arayüz Dili",
|
||||
"Translation": "Çeviri",
|
||||
"Enable Translation": "Çeviriyi Etkinleştir",
|
||||
"Translation Service": "Çeviri Hizmeti",
|
||||
"Translate To": "Şuna Çevir",
|
||||
"Disable Translation": "Çeviriyi Devre Dışı Bırak",
|
||||
"Overlap Margin": "Çakışma Kenar Boşluğu"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Розмір файлу:",
|
||||
"Auto Detect": "Авто виявлення",
|
||||
"Touch": "Дотик",
|
||||
"Scrolling Overlap (px)": "Прокрутка перекриття (px)",
|
||||
"Next Section": "Наступний розділ",
|
||||
"Previous Section": "Попередній розділ",
|
||||
"Next Page": "Наступна сторінка",
|
||||
@@ -325,5 +324,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Інвертувати зображення в темряві",
|
||||
"Help improve Readest": "Допоможіть покращити Readest",
|
||||
"Sharing anonymized statistics": "Поділ анонімною статистикою"
|
||||
"Sharing anonymized statistics": "Поділ анонімною статистикою",
|
||||
"Interface Language": "Мова інтерфейсу",
|
||||
"Translation": "Переклад",
|
||||
"Enable Translation": "Увімкнути переклад",
|
||||
"Translation Service": "Служба перекладу",
|
||||
"Translate To": "Перекласти на",
|
||||
"Disable Translation": "Вимкнути переклад",
|
||||
"Overlap Margin": "Відступ перекриття"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "Kích thước tệp:",
|
||||
"Auto Detect": "Phát hiện tự động",
|
||||
"Touch": "Chạm",
|
||||
"Scrolling Overlap (px)": "Chồng chéo cuộn (px)",
|
||||
"Next Section": "Phần tiếp theo",
|
||||
"Previous Section": "Phần trước",
|
||||
"Next Page": "Trang tiếp theo",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Đảo ảnh chế độ tối",
|
||||
"Help improve Readest": "Giúp cải thiện Readest",
|
||||
"Sharing anonymized statistics": "Chia sẻ thống kê ẩn danh"
|
||||
"Sharing anonymized statistics": "Chia sẻ thống kê ẩn danh",
|
||||
"Interface Language": "Ngôn ngữ giao diện",
|
||||
"Translation": "Dịch",
|
||||
"Enable Translation": "Bật dịch",
|
||||
"Translation Service": "Dịch vụ dịch thuật",
|
||||
"Translate To": "Dịch sang",
|
||||
"Disable Translation": "Tắt dịch",
|
||||
"Overlap Margin": "Độ chồng lề"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "文件大小",
|
||||
"Auto Detect": "自动检测",
|
||||
"Touch": "触控",
|
||||
"Scrolling Overlap (px)": "滚动重叠(px)",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next Page": "下一页",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主题下图片反色",
|
||||
"Help improve Readest": "帮助改进 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名统计数据"
|
||||
"Sharing anonymized statistics": "分享匿名统计数据",
|
||||
"Interface Language": "界面语言",
|
||||
"Translation": "翻译",
|
||||
"Enable Translation": "启用翻译",
|
||||
"Translation Service": "翻译服务",
|
||||
"Translate To": "翻译到",
|
||||
"Disable Translation": "禁用翻译",
|
||||
"Overlap Margin": "重叠边距"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"File Size:": "檔案大小",
|
||||
"Auto Detect": "自動檢測",
|
||||
"Touch": "觸控",
|
||||
"Scrolling Overlap (px)": "滾動重疊(px)",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next Page": "下一頁",
|
||||
@@ -322,5 +321,12 @@
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主題下圖片反相",
|
||||
"Help improve Readest": "幫助改進 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名統計數據"
|
||||
"Sharing anonymized statistics": "分享匿名統計數據",
|
||||
"Interface Language": "介面語言",
|
||||
"Translation": "翻譯",
|
||||
"Enable Translation": "啟用翻譯",
|
||||
"Translation Service": "翻譯服務",
|
||||
"Translate To": "翻譯至",
|
||||
"Disable Translation": "停用翻譯",
|
||||
"Overlap Margin": "重疊邊距"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { getMaxInlineSize } from '@/utils/config';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
|
||||
const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
@@ -52,6 +53,7 @@ const FoliateViewer: React.FC<{
|
||||
useUICSS(bookKey);
|
||||
useProgressSync(bookKey);
|
||||
useProgressAutoSave(bookKey);
|
||||
useTextTranslation(bookKey, viewRef.current);
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
|
||||
@@ -14,6 +14,7 @@ import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import NotebookToggler from './NotebookToggler';
|
||||
import SettingsToggler from './SettingsToggler';
|
||||
import TranslationToggler from './TranslationToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface HeaderBarProps {
|
||||
@@ -129,6 +130,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
|
||||
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
|
||||
<SettingsToggler />
|
||||
<TranslationToggler bookKey={bookKey} />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { RiTranslateAi } from 'react-icons/ri';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { saveViewSettings } from '../utils/viewSettingsHelper';
|
||||
import Button from '@/components/Button';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
||||
const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { getViewSettings, setViewSettings, setHoveredBookKey } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
|
||||
|
||||
useEffect(() => {
|
||||
if (translationEnabled === viewSettings.translationEnabled) return;
|
||||
setHoveredBookKey('');
|
||||
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
|
||||
viewSettings.translationEnabled = translationEnabled;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={
|
||||
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
|
||||
}
|
||||
onClick={() => setTranslationEnabled(!translationEnabled)}
|
||||
tooltip={translationEnabled ? _('Disable Translation') : _('Enable Translation')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TranslationToggler;
|
||||
@@ -6,7 +6,8 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import { TranslatorName } from '@/services/translators';
|
||||
import { UseTranslatorOptions } from '@/services/translators';
|
||||
import { localeToLang } from '@/utils/lang';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
const notSupportedLangs = ['hi', 'vi'];
|
||||
@@ -16,12 +17,7 @@ const generateTranslatorLangs = () => {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [code, name] of Object.entries(langs)) {
|
||||
if (notSupportedLangs.includes(code)) continue;
|
||||
let newCode = code.toUpperCase();
|
||||
if (newCode === 'ZH-CN') {
|
||||
newCode = 'ZH-HANS';
|
||||
} else if (newCode === 'ZH-TW') {
|
||||
newCode = 'ZH-HANT';
|
||||
}
|
||||
const newCode = localeToLang(code).toUpperCase();
|
||||
result[newCode] = name;
|
||||
}
|
||||
return result;
|
||||
@@ -53,11 +49,9 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
const { token } = useAuth();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const [providers, setProviders] = useState<TranslatorType[]>([]);
|
||||
const [provider, setProvider] = useState<TranslatorName>(
|
||||
settings.globalReadSettings.translationProvider as TranslatorName,
|
||||
);
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
|
||||
const [provider, setProvider] = useState(settings.globalReadSettings.translationProvider);
|
||||
const [translation, setTranslation] = useState<string | null>(null);
|
||||
const [detectedSourceLang, setDetectedSourceLang] = useState<
|
||||
keyof typeof TRANSLATOR_LANGS | null
|
||||
@@ -69,7 +63,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
provider,
|
||||
sourceLang,
|
||||
targetLang,
|
||||
});
|
||||
} as UseTranslatorOptions);
|
||||
|
||||
const handleSourceLangChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSourceLang(event.target.value);
|
||||
@@ -86,7 +80,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
if (selectedTranslator) {
|
||||
settings.globalReadSettings.translationProvider = selectedTranslator.name;
|
||||
setSettings(settings);
|
||||
setProvider(selectedTranslator.name as TranslatorName);
|
||||
setProvider(selectedTranslator.name);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
/>
|
||||
</div>
|
||||
<NumberInput
|
||||
label={_('Scrolling Overlap (px)')}
|
||||
label={_('Overlap Margin')}
|
||||
value={scrollingOverlap}
|
||||
onChange={setScrollingOverlap}
|
||||
disabled={!viewSettings.scrolled}
|
||||
|
||||
@@ -8,17 +8,30 @@ interface DropDownProps {
|
||||
selected: { option: string; label: string };
|
||||
options: { option: string; label: string }[];
|
||||
onSelect: (option: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
listClassName?: string;
|
||||
}
|
||||
|
||||
const DropDown: React.FC<DropDownProps> = ({ selected, options, onSelect }) => {
|
||||
const DropDown: React.FC<DropDownProps> = ({
|
||||
selected,
|
||||
options,
|
||||
onSelect,
|
||||
className,
|
||||
listClassName,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const defaultIconSize = useDefaultIconSize();
|
||||
|
||||
return (
|
||||
<div className='dropdown dropdown-bottom'>
|
||||
<div className={clsx('dropdown dropdown-bottom', className)}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
className={clsx(
|
||||
'btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case',
|
||||
disabled && 'btn-disabled',
|
||||
)}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<span>{selected.label}</span>
|
||||
@@ -29,6 +42,7 @@ const DropDown: React.FC<DropDownProps> = ({ selected, options, onSelect }) => {
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute z-[1] shadow',
|
||||
'menu-vertical right-[-32px] mt-2 inline max-h-80 w-44 overflow-y-scroll sm:right-0',
|
||||
listClassName,
|
||||
)}
|
||||
>
|
||||
{options.map(({ option, label }) => (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
import { getTranslators } from '@/services/translators';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import cssValidate from '@/utils/css';
|
||||
@@ -26,8 +27,11 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
|
||||
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
|
||||
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
|
||||
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [inputFocusInAndroid, setInputFocusInAndroid] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
@@ -108,7 +112,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getUILangOptions = () => {
|
||||
const getLangOptions = () => {
|
||||
const langs = TRANSLATED_LANGS as Record<string, string>;
|
||||
const options = Object.entries(langs).map(([option, label]) => ({ option, label }));
|
||||
options.sort((a, b) => a.label.localeCompare(b.label));
|
||||
@@ -121,6 +125,40 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
i18n.changeLanguage(option ? option : navigator.language);
|
||||
};
|
||||
|
||||
const getTranslationProviderOptions = () => {
|
||||
const translators = getTranslators();
|
||||
const availableProviders = translators.map((t) => {
|
||||
return { option: t.name, label: t.label };
|
||||
});
|
||||
return availableProviders;
|
||||
};
|
||||
|
||||
const getCurrentTranslationProviderOption = () => {
|
||||
const option = translationProvider;
|
||||
const availableProviders = getTranslationProviderOptions();
|
||||
return availableProviders.find((p) => p.option === option) || availableProviders[0]!;
|
||||
};
|
||||
|
||||
const handleSelectTranslationProvider = (option: string) => {
|
||||
setTranslationProvider(option);
|
||||
saveViewSettings(envConfig, bookKey, 'translationProvider', option, false, false);
|
||||
viewSettings.translationProvider = option;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
};
|
||||
|
||||
const getCurrentTargetLangOption = () => {
|
||||
const option = translateTargetLang;
|
||||
const availableOptions = getLangOptions();
|
||||
return availableOptions.find((o) => o.option === option) || availableOptions[0]!;
|
||||
};
|
||||
|
||||
const handleSelectTargetLang = (option: string) => {
|
||||
setTranslateTargetLang(option);
|
||||
saveViewSettings(envConfig, bookKey, 'translateTargetLang', option, false, false);
|
||||
viewSettings.translateTargetLang = option;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'animated', animated, false, false);
|
||||
if (animated) {
|
||||
@@ -139,6 +177,14 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [screenOrientation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (translationEnabled === viewSettings.translationEnabled) return;
|
||||
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
|
||||
viewSettings.translationEnabled = translationEnabled;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -151,10 +197,10 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Language')}</span>
|
||||
<span className=''>{_('Interface Language')}</span>
|
||||
<DropDown
|
||||
options={getLangOptions()}
|
||||
selected={getCurrentUILangOption()}
|
||||
options={getUILangOptions()}
|
||||
onSelect={handleSelectUILang}
|
||||
/>
|
||||
</div>
|
||||
@@ -162,6 +208,46 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Translation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Enable Translation')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={translationEnabled}
|
||||
onChange={() => setTranslationEnabled(!translationEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Translation Service')}</span>
|
||||
<DropDown
|
||||
selected={getCurrentTranslationProviderOption()}
|
||||
options={getTranslationProviderOptions()}
|
||||
onSelect={handleSelectTranslationProvider}
|
||||
disabled={!translationEnabled}
|
||||
className='dropdown-top'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Translate To')}</span>
|
||||
<DropDown
|
||||
options={getLangOptions()}
|
||||
selected={getCurrentTargetLangOption()}
|
||||
onSelect={handleSelectTargetLang}
|
||||
disabled={!translationEnabled}
|
||||
className='dropdown-top'
|
||||
listClassName='!max-h-60'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
|
||||
@@ -141,8 +141,8 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
<div
|
||||
ref={tabsRef}
|
||||
className={clsx(
|
||||
'dialog-tabs flex h-10 w-full items-center',
|
||||
showTabLabels ? 'gap-4 sm:gap-2' : 'gap-4',
|
||||
'dialog-tabs ms-2 flex h-10 w-full items-center',
|
||||
showTabLabels ? 'gap-2' : 'gap-4',
|
||||
)}
|
||||
>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { UseTranslatorOptions } from '@/services/translators';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslator } from '@/hooks/useTranslator';
|
||||
import { walkTextNodes } from '@/utils/walk';
|
||||
import { localeToLang } from '@/utils/lang';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
export function useTextTranslation(bookKey: string, view: FoliateView | null) {
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const enabled = useRef(viewSettings.translationEnabled);
|
||||
const [provider, setProvider] = useState(viewSettings.translationProvider);
|
||||
const [targetLang, setTargetLang] = useState(viewSettings.translateTargetLang);
|
||||
|
||||
const { translate } = useTranslator({
|
||||
provider,
|
||||
targetLang: localeToLang(targetLang || getLocale()),
|
||||
} as UseTranslatorOptions);
|
||||
|
||||
const translateRef = useRef(translate);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const translatedElements = useRef<HTMLElement[]>([]);
|
||||
const allTextNodes = useRef<HTMLElement[]>([]);
|
||||
|
||||
const toggleTranslationVisibility = (visible: boolean) => {
|
||||
translatedElements.current.forEach((element) => {
|
||||
const translationTargets = element.querySelectorAll('.translation-target');
|
||||
translationTargets.forEach((target) => {
|
||||
if (visible) {
|
||||
target.classList.remove('hidden');
|
||||
} else {
|
||||
target.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
translateRef.current = translate;
|
||||
}, [translate]);
|
||||
|
||||
const updateTranslation = () => {
|
||||
translatedElements.current.forEach((element) => {
|
||||
const translationTargets = element.querySelectorAll('.translation-target');
|
||||
translationTargets.forEach((target) => target.remove());
|
||||
});
|
||||
|
||||
translatedElements.current = [];
|
||||
if (viewSettings.translationEnabled && view) {
|
||||
recreateTranslationObserver();
|
||||
}
|
||||
};
|
||||
|
||||
const createTranslationObserver = () => {
|
||||
return new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
translateElement(entry.target as HTMLElement);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '2560px',
|
||||
threshold: 0.01,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const recreateTranslationObserver = () => {
|
||||
const observer = createTranslationObserver();
|
||||
observerRef.current?.disconnect();
|
||||
observerRef.current = observer;
|
||||
allTextNodes.current.forEach((el) => observer.observe(el));
|
||||
};
|
||||
|
||||
const translateElement = async (el: HTMLElement) => {
|
||||
if (!enabled.current) return;
|
||||
const text = el.textContent?.trim();
|
||||
if (!text) return;
|
||||
|
||||
if (el.querySelector('.translation-target')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const translated = await translateRef.current([text]);
|
||||
const translatedText = translated[0];
|
||||
if (!translatedText || text === translatedText) return;
|
||||
|
||||
const wrapper = document.createElement('font');
|
||||
wrapper.className = `translation-target ${!enabled.current ? 'hidden' : ''}`;
|
||||
wrapper.setAttribute('translation-element-mark', '1');
|
||||
wrapper.setAttribute('lang', targetLang || getLocale());
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
|
||||
const blockWrapper = document.createElement('font');
|
||||
blockWrapper.className = 'translation-target translation-block-wrapper';
|
||||
|
||||
const inner = document.createElement('font');
|
||||
inner.className = 'translation-target target-inner target-inner-theme-none';
|
||||
inner.textContent = translatedText;
|
||||
|
||||
blockWrapper.appendChild(inner);
|
||||
wrapper.appendChild(blockWrapper);
|
||||
|
||||
el.appendChild(wrapper);
|
||||
translatedElements.current.push(el);
|
||||
} catch (err) {
|
||||
console.warn('Translation failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewSettings) return;
|
||||
|
||||
const enabledChanged = enabled.current !== viewSettings.translationEnabled;
|
||||
const providerChanged = provider !== viewSettings.translationProvider;
|
||||
const targetLangChanged = targetLang !== viewSettings.translateTargetLang;
|
||||
|
||||
if (enabledChanged) {
|
||||
enabled.current = viewSettings.translationEnabled;
|
||||
}
|
||||
|
||||
if (providerChanged) {
|
||||
setProvider(viewSettings.translationProvider);
|
||||
}
|
||||
|
||||
if (targetLangChanged) {
|
||||
setTargetLang(viewSettings.translateTargetLang);
|
||||
}
|
||||
|
||||
if (enabledChanged) {
|
||||
toggleTranslationVisibility(viewSettings.translationEnabled);
|
||||
if (enabled.current) {
|
||||
recreateTranslationObserver();
|
||||
}
|
||||
} else if (providerChanged || targetLangChanged) {
|
||||
updateTranslation();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKey, viewSettings, provider, targetLang]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!view) return;
|
||||
|
||||
const onLoad = () => {
|
||||
const observer = createTranslationObserver();
|
||||
observerRef.current = observer;
|
||||
const nodes = walkTextNodes(view);
|
||||
allTextNodes.current = nodes;
|
||||
nodes.forEach((el) => observer.observe(el));
|
||||
};
|
||||
|
||||
view.addEventListener('load', onLoad);
|
||||
return () => {
|
||||
view.removeEventListener('load', onLoad);
|
||||
observerRef.current?.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [view]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
observerRef.current?.disconnect();
|
||||
translatedElements.current = [];
|
||||
};
|
||||
}, [view]);
|
||||
}
|
||||
@@ -127,6 +127,7 @@ export function useTranslator({
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[provider, sourceLang, targetLang, translator, token],
|
||||
);
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ import {
|
||||
DEFAULT_CJK_VIEW_SETTINGS,
|
||||
DEFAULT_MOBILE_READSETTINGS,
|
||||
DEFAULT_SCREEN_CONFIG,
|
||||
DEFAULT_TRANSLATOR_CONFIG,
|
||||
} from './constants';
|
||||
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getOSPlatform, getTargetLang, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
|
||||
import { ClosableFile } from '@/utils/file';
|
||||
@@ -97,6 +98,7 @@ export abstract class BaseAppService implements AppService {
|
||||
...DEFAULT_VIEW_CONFIG,
|
||||
...DEFAULT_TTS_CONFIG,
|
||||
...DEFAULT_SCREEN_CONFIG,
|
||||
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
|
||||
...settings.globalViewSettings,
|
||||
};
|
||||
} catch {
|
||||
@@ -117,6 +119,7 @@ export abstract class BaseAppService implements AppService {
|
||||
...DEFAULT_VIEW_CONFIG,
|
||||
...DEFAULT_TTS_CONFIG,
|
||||
...DEFAULT_SCREEN_CONFIG,
|
||||
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
|
||||
},
|
||||
} as SystemSettings;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BookStyle,
|
||||
HighlightColor,
|
||||
ScreenConfig,
|
||||
TranslatorConfig,
|
||||
TTSConfig,
|
||||
ViewConfig,
|
||||
ViewSettings,
|
||||
@@ -147,6 +148,12 @@ export const DEFAULT_TTS_CONFIG: TTSConfig = {
|
||||
ttsVoice: '',
|
||||
};
|
||||
|
||||
export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
translationEnabled: false,
|
||||
translationProvider: 'deepl',
|
||||
translateTargetLang: '',
|
||||
};
|
||||
|
||||
export const DEFAULT_SCREEN_CONFIG: ScreenConfig = {
|
||||
screenOrientation: 'auto',
|
||||
};
|
||||
|
||||
@@ -24,8 +24,8 @@ export const deeplProvider: TranslationProvider = {
|
||||
|
||||
const body = JSON.stringify({
|
||||
text: text,
|
||||
source_lang: sourceLang,
|
||||
target_lang: targetLang,
|
||||
source_lang: sourceLang.toUpperCase(),
|
||||
target_lang: targetLang.toUpperCase(),
|
||||
use_cache: useCache,
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export class TTSController extends EventTarget {
|
||||
#preprocessSSML(ssml?: string) {
|
||||
if (!ssml) return;
|
||||
ssml = ssml
|
||||
.replace(/<emphasis[^>]*>([^<]+)<\/emphasis>/g, '$1')
|
||||
.replace(/[–—]/g, ',')
|
||||
.replace('<break/>', ' ')
|
||||
.replace(/\.{3,}/g, ' ')
|
||||
|
||||
@@ -127,6 +127,12 @@ export interface TTSConfig {
|
||||
ttsVoice: string;
|
||||
}
|
||||
|
||||
export interface TranslatorConfig {
|
||||
translationEnabled: boolean;
|
||||
translationProvider: string;
|
||||
translateTargetLang: string;
|
||||
}
|
||||
|
||||
export interface ScreenConfig {
|
||||
screenOrientation: 'auto' | 'portrait' | 'landscape';
|
||||
}
|
||||
@@ -137,6 +143,7 @@ export interface ViewSettings
|
||||
BookFont,
|
||||
ViewConfig,
|
||||
TTSConfig,
|
||||
TranslatorConfig,
|
||||
ScreenConfig {}
|
||||
|
||||
export interface BookProgress {
|
||||
|
||||
@@ -26,3 +26,16 @@ export const langToDefaultLocale = (langCode: string): string => {
|
||||
|
||||
return mapping[langCode] || langCode;
|
||||
};
|
||||
|
||||
export const localeToLang = (locale: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
'zh-HK': 'zh-Hant',
|
||||
};
|
||||
|
||||
if (locale.startsWith('zh')) {
|
||||
return mapping[locale] || 'zh-Hans';
|
||||
}
|
||||
return mapping[locale] || locale.split('-')[0]!.toLowerCase();
|
||||
};
|
||||
|
||||
@@ -40,6 +40,14 @@ export const getUserLang = () => {
|
||||
return locale.split('-')[0] || 'en';
|
||||
};
|
||||
|
||||
export const getTargetLang = () => {
|
||||
const locale = getLocale();
|
||||
if (locale.startsWith('zh')) {
|
||||
return locale === 'zh-Hant' || locale === 'zh-HK' || locale === 'zh-TW' ? 'zh-Hant' : 'zh-Hans';
|
||||
}
|
||||
return locale.split('-')[0] || 'en';
|
||||
};
|
||||
|
||||
export const isCJKEnv = () => {
|
||||
const browserLanguage = navigator.language || '';
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
|
||||
@@ -353,6 +353,16 @@ export const getFootnoteStyles = () => `
|
||||
}
|
||||
`;
|
||||
|
||||
const getTranslationStyles = () => `
|
||||
.translation-block-wrapper {
|
||||
display: inline-block !important;
|
||||
margin: 0.5em 0 !important;
|
||||
}
|
||||
.translation-target.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ThemeCode {
|
||||
bg: string;
|
||||
fg: string;
|
||||
@@ -432,8 +442,9 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
|
||||
viewSettings.overrideFont!,
|
||||
themeCode,
|
||||
);
|
||||
const translationStyles = getTranslationStyles();
|
||||
const userStylesheet = viewSettings.userStylesheet!;
|
||||
return `${layoutStyles}\n${fontStyles}\n${userStylesheet}`;
|
||||
return `${layoutStyles}\n${fontStyles}\n${translationStyles}\n${userStylesheet}`;
|
||||
};
|
||||
|
||||
export const mountAdditionalFonts = (document: Document) => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export const walkTextNodes = (root: HTMLElement): HTMLElement[] => {
|
||||
const elements: HTMLElement[] = [];
|
||||
const walk = (node: HTMLElement | Document | ShadowRoot, depth = 0) => {
|
||||
if (depth > 15) return;
|
||||
if (node instanceof HTMLElement && node.shadowRoot) {
|
||||
walk(node.shadowRoot, depth + 1);
|
||||
}
|
||||
const children = 'children' in node ? (Array.from(node.children) as HTMLElement[]) : [];
|
||||
for (const child of children) {
|
||||
if (child.tagName === 'STYLE' || child.tagName === 'LINK') {
|
||||
continue;
|
||||
}
|
||||
if (child.shadowRoot) {
|
||||
walk(child.shadowRoot, depth + 1);
|
||||
}
|
||||
if (child.tagName === 'IFRAME') {
|
||||
const iframe = child as HTMLIFrameElement;
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||
if (iframeDoc && iframeDoc.body) {
|
||||
walk(iframeDoc.body, depth + 1);
|
||||
}
|
||||
}
|
||||
const hasDirectText =
|
||||
child.childNodes &&
|
||||
Array.from(child.childNodes).some(
|
||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
|
||||
);
|
||||
if (child.children.length === 0 && child.textContent?.trim()) {
|
||||
elements.push(child);
|
||||
} else if (hasDirectText) {
|
||||
elements.push(child);
|
||||
} else if (child.children.length > 0) {
|
||||
walk(child, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(root);
|
||||
return elements;
|
||||
};
|
||||
Reference in New Issue
Block a user