From 86efcde927b62e7677f06e04f81be04e191b5ec2 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 24 Oct 2025 01:34:43 +0800 Subject: [PATCH] feat(tts): add an option to select target language for TTS on translated books (#2310) --- .../public/locales/ar/translation.json | 6 +- .../public/locales/bn/translation.json | 6 +- .../public/locales/bo/translation.json | 6 +- .../public/locales/de/translation.json | 6 +- .../public/locales/el/translation.json | 6 +- .../public/locales/es/translation.json | 6 +- .../public/locales/fr/translation.json | 6 +- .../public/locales/hi/translation.json | 6 +- .../public/locales/id/translation.json | 6 +- .../public/locales/it/translation.json | 6 +- .../public/locales/ja/translation.json | 6 +- .../public/locales/ko/translation.json | 6 +- .../public/locales/nl/translation.json | 6 +- .../public/locales/pl/translation.json | 6 +- .../public/locales/pt/translation.json | 6 +- .../public/locales/ru/translation.json | 6 +- .../public/locales/si/translation.json | 6 +- .../public/locales/sv/translation.json | 6 +- .../public/locales/ta/translation.json | 6 +- .../public/locales/th/translation.json | 6 +- .../public/locales/tr/translation.json | 6 +- .../public/locales/uk/translation.json | 6 +- .../public/locales/vi/translation.json | 6 +- .../public/locales/zh-CN/translation.json | 6 +- .../public/locales/zh-TW/translation.json | 6 +- .../reader/components/TranslationToggler.tsx | 2 +- .../app/reader/components/tts/TTSControl.tsx | 23 ++++++++ .../src/components/settings/LangPanel.tsx | 35 +++++++++++- apps/readest-app/src/services/constants.ts | 1 + .../src/services/tts/TTSController.ts | 10 +++- apps/readest-app/src/types/book.ts | 1 + apps/readest-app/src/utils/reload.ts | 2 - apps/readest-app/src/utils/ssml.ts | 56 +++++++++++++++++++ 33 files changed, 248 insertions(+), 32 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index df685b66..fdad6af1 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -632,5 +632,9 @@ "Tap Both Sides": "اضغط على كلا الجانبين", "Click Both Sides": "انقر على كلا الجانبين", "Swap Tap Sides": "تبديل جانبي اللمس", - "Swap Click Sides": "تبديل جانبي النقر" + "Swap Click Sides": "تبديل جانبي النقر", + "Source and Translated": "المصدر والمترجم", + "Translated Only": "المترجم فقط", + "Source Only": "المصدر فقط", + "TTS Text": "نص تحويل النص إلى كلام" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index f5de8690..15dfd6d9 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "দুই পাশে ট্যাপ করুন", "Click Both Sides": "দুই পাশে ক্লিক করুন", "Swap Tap Sides": "ট্যাপের পাশে পরিবর্তন করুন", - "Swap Click Sides": "ক্লিকের পাশে পরিবর্তন করুন" + "Swap Click Sides": "ক্লিকের পাশে পরিবর্তন করুন", + "Source and Translated": "উৎস এবং অনূদিত", + "Translated Only": "শুধুমাত্র অনূদিত", + "Source Only": "শুধুমাত্র উৎস", + "TTS Text": "TTS টেক্সট" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index cfc1c234..701b4e5c 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "གཡས་གཉིས་ལ་ཐོག་འགྲོ།", "Click Both Sides": "གཡས་གཉིས་ལ་ཁྱེད་ལུས་འགྲོ།", "Swap Tap Sides": "ཐོག་གཡས་གཉིས་བརྗེ་བ།", - "Swap Click Sides": "ཁྱེད་ལུས་གཡས་གཉིས་བརྗེ་བ།" + "Swap Click Sides": "ཁྱེད་ལུས་གཡས་གཉིས་བརྗེ་བ།", + "Source and Translated": "ཡིག་སྒྱུར་སྤོ་བ།", + "Translated Only": "མདོག་ཚད་ཀྱི་བསྡུར་བ།", + "Source Only": "ཡིག་སྒྱུར་སྤོ་བ།", + "TTS Text": "TTS ཡིག" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 60f1179e..921329bd 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "Tippen Sie auf beide Seiten", "Click Both Sides": "Klicken Sie auf beide Seiten", "Swap Tap Sides": "Seiten für das Tippen tauschen", - "Swap Click Sides": "Seiten für das Klicken tauschen" + "Swap Click Sides": "Seiten für das Klicken tauschen", + "Source and Translated": "Quell- und Übersetzungstext", + "Translated Only": "Nur Übersetzungstext", + "Source Only": "Nur Quelltext", + "TTS Text": "TTS-Text" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 25f0a400..53d894a2 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "Ταπ και στις δύο πλευρές", "Click Both Sides": "Κλικ και στις δύο πλευρές", "Swap Tap Sides": "Ανταλλαγή πλευρών ταπ", - "Swap Click Sides": "Ανταλλαγή πλευρών κλικ" + "Swap Click Sides": "Ανταλλαγή πλευρών κλικ", + "Source and Translated": "Πηγή και Μεταφρασμένο", + "Translated Only": "Μόνο Μεταφρασμένο", + "Source Only": "Μόνο πηγή", + "TTS Text": "Κείμενο TTS" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 24346c2c..e0dd5a20 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -620,5 +620,9 @@ "Tap Both Sides": "Tocar Ambos Lados", "Click Both Sides": "Hacer Clic en Ambos Lados", "Swap Tap Sides": "Intercambiar Lados de Toque", - "Swap Click Sides": "Intercambiar Lados de Clic" + "Swap Click Sides": "Intercambiar Lados de Clic", + "Source and Translated": "Fuente y Traducido", + "Translated Only": "Solo Traducido", + "Source Only": "Solo Fuente", + "TTS Text": "TTS Texto" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index a0d2919b..ee69504c 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -620,5 +620,9 @@ "Tap Both Sides": "Tapoter des Deux Côtés", "Click Both Sides": "Cliquer des Deux Côtés", "Swap Tap Sides": "Échanger les Côtés de Tap", - "Swap Click Sides": "Échanger les Côtés de Clic" + "Swap Click Sides": "Échanger les Côtés de Clic", + "Source and Translated": "Source et Traduit", + "Translated Only": "Traduit Seulement", + "Source Only": "Source Seulement", + "TTS Text": "TTS Texte" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index d61bd368..d9803581 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "दोनों पक्षों पर टैप करें", "Click Both Sides": "दोनों पक्षों पर क्लिक करें", "Swap Tap Sides": "स्वैप टैप साइड्स", - "Swap Click Sides": "स्वैप क्लिक साइड्स" + "Swap Click Sides": "स्वैप क्लिक साइड्स", + "Source and Translated": "स्रोत और अनुवादित", + "Translated Only": "केवल अनुवादित", + "Source Only": "केवल स्रोत", + "TTS Text": "TTS टेक्स्ट" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index a60eb6fd..2b9d9f79 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "Ketuk Kedua Sisi", "Click Both Sides": "Klik Kedua Sisi", "Swap Tap Sides": "Tukar Sisi Ketuk", - "Swap Click Sides": "Tukar Sisi Klik" + "Swap Click Sides": "Tukar Sisi Klik", + "Source and Translated": "Sumber dan Terjemahan", + "Translated Only": "Hanya Terjemahan", + "Source Only": "Hanya Sumber", + "TTS Text": "Teks TTS" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 050378c2..cb4a555f 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -620,5 +620,9 @@ "Tap Both Sides": "Tocca Entrambi i Lati", "Click Both Sides": "Clicca Entrambi i Lati", "Swap Tap Sides": "Scambia Sides Tap", - "Swap Click Sides": "Scambia Sides Click" + "Swap Click Sides": "Scambia Sides Click", + "Source and Translated": "Fonte e Tradotto", + "Translated Only": "Solo Tradotto", + "Source Only": "Solo Fonte", + "TTS Text": "Testo TTS" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 156024c4..50b93c48 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "両側をタップ", "Click Both Sides": "両側をクリック", "Swap Tap Sides": "タップサイドを入れ替える", - "Swap Click Sides": "クリックサイドを入れ替える" + "Swap Click Sides": "クリックサイドを入れ替える", + "Source and Translated": "原文と翻訳", + "Translated Only": "翻訳のみ", + "Source Only": "原文のみ", + "TTS Text": "TTSテキスト" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 32eee3b7..be5d9033 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "양쪽을 탭", "Click Both Sides": "양쪽을 클릭", "Swap Tap Sides": "탭 사이드 전환", - "Swap Click Sides": "클릭 사이드 전환" + "Swap Click Sides": "클릭 사이드 전환", + "Source and Translated": "원본 및 번역", + "Translated Only": "번역만", + "Source Only": "원본만", + "TTS Text": "TTS 텍스트" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 67066bd2..2ba5acb7 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "Tik op Beide Zijden", "Click Both Sides": "Klik op Beide Zijden", "Swap Tap Sides": "Wissel Tap Zijden", - "Swap Click Sides": "Wissel Klik Zijden" + "Swap Click Sides": "Wissel Klik Zijden", + "Source and Translated": "Bron en Vertaling", + "Translated Only": "Alleen Vertaling", + "Source Only": "Alleen Bron", + "TTS Text": "TTS Tekst" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 744b5e85..64e2096c 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -624,5 +624,9 @@ "Tap Both Sides": "Dotknij obu stron", "Click Both Sides": "Kliknij obu stron", "Swap Tap Sides": "Wymień Tap Zewnętrzne", - "Swap Click Sides": "Wymień Click Zewnętrzne" + "Swap Click Sides": "Wymień Click Zewnętrzne", + "Source and Translated": "Źródło i Tłumaczenie", + "Translated Only": "Tylko Tłumaczenie", + "Source Only": "Tylko Źródło", + "TTS Text": "Tekst TTS" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 3869aeb8..4bbdcb5a 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -620,5 +620,9 @@ "Tap Both Sides": "Toque em Ambos os Lados", "Click Both Sides": "Clique em Ambos os Lados", "Swap Tap Sides": "Trocar Lados do Toque", - "Swap Click Sides": "Trocar Lados do Clique" + "Swap Click Sides": "Trocar Lados do Clique", + "Source and Translated": "Fonte e Traduzido", + "Translated Only": "Apenas Traduzido", + "Source Only": "Apenas Fonte", + "TTS Text": "TTS Texto" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 7468521a..dc1fecc7 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -624,5 +624,9 @@ "Tap Both Sides": "Тапните обе стороны", "Click Both Sides": "Нажмите обе стороны", "Swap Tap Sides": "Переключить стороны касания", - "Swap Click Sides": "Переключить стороны клика" + "Swap Click Sides": "Переключить стороны клика", + "Source and Translated": "Источник и перевод", + "Translated Only": "Только перевод", + "Source Only": "Только источник", + "TTS Text": "TTS Текст" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index f79ace6f..4b50f24c 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "පැත්ත දෙකම ටැප් කරන්න", "Click Both Sides": "පැත්ත දෙකම ක්ලික් කරන්න", "Swap Tap Sides": "පැත්ත දෙකම ටැප් කරන්න", - "Swap Click Sides": "පැත්ත දෙකම ක්ලික් කරන්න" + "Swap Click Sides": "පැත්ත දෙකම ක්ලික් කරන්න", + "Source and Translated": "මූලාශ්‍ර සහ පරිවර්තනය", + "Translated Only": "පරිවර්තනය පමණක්", + "Source Only": "මූලාශ්‍රය පමණක්", + "TTS Text": "TTS පෙළ" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 5acddc6e..f3900e56 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "Tryck på båda sidor", "Click Both Sides": "Klicka på båda sidor", "Swap Tap Sides": "Byt plats på tap-sidor", - "Swap Click Sides": "Byt plats på klick-sidor" + "Swap Click Sides": "Byt plats på klick-sidor", + "Source and Translated": "Källa och översatt", + "Translated Only": "Översatt endast", + "Source Only": "Endast källa", + "TTS Text": "TTS-text" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 93fc0393..447cd397 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "இரு பக்கங்களையும் தொடுங்கள்", "Click Both Sides": "இரு பக்கங்களையும் கிளிக் செய்யவும்", "Swap Tap Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்", - "Swap Click Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்" + "Swap Click Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்", + "Source and Translated": "மூல மற்றும் மொழிபெயர்க்கப்பட்டது", + "Translated Only": "மொழிபெயர்க்கப்பட்டது மட்டும்", + "Source Only": "மூல மட்டும்", + "TTS Text": "TTS உரை" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 77b07aad..df506194 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "แตะทั้งสองด้าน", "Click Both Sides": "คลิกทั้งสองด้าน", "Swap Tap Sides": "เปลี่ยนด้านการแตะ", - "Swap Click Sides": "เปลี่ยนด้านการคลิก" + "Swap Click Sides": "เปลี่ยนด้านการคลิก", + "Source and Translated": "ต้นฉบับและแปลแล้ว", + "Translated Only": "แปลแล้วเท่านั้น", + "Source Only": "ต้นฉบับเท่านั้น", + "TTS Text": "ข้อความ TTS" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index de5fae92..d4cdeb5a 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -616,5 +616,9 @@ "Tap Both Sides": "Her İki Tarafa Dokunun", "Click Both Sides": "Her İki Tarafa Tıklayın", "Swap Tap Sides": "Dokunma Taraflarını Değiştir", - "Swap Click Sides": "Tıklama Taraflarını Değiştir" + "Swap Click Sides": "Tıklama Taraflarını Değiştir", + "Source and Translated": "Kaynak ve Çeviri", + "Translated Only": "Yalnızca Çeviri", + "Source Only": "Yalnızca Kaynak", + "TTS Text": "TTS Metni" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 86ab8417..7ba40c5b 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -624,5 +624,9 @@ "Tap Both Sides": "Натисніть обидві сторони", "Click Both Sides": "Клікніть обидві сторони", "Swap Tap Sides": "Змінити сторони дотику", - "Swap Click Sides": "Змінити сторони кліку" + "Swap Click Sides": "Змінити сторони кліку", + "Source and Translated": "Джерело та переклад", + "Translated Only": "Тільки переклад", + "Source Only": "Тільки джерело", + "TTS Text": "Текст TTS" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 6ecaceb1..b6948cbc 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "Chạm vào cả hai bên", "Click Both Sides": "Nhấp vào cả hai bên", "Swap Tap Sides": "Đổi bên chạm", - "Swap Click Sides": "Đổi bên nhấp" + "Swap Click Sides": "Đổi bên nhấp", + "Source and Translated": "Nguồn và Đã dịch", + "Translated Only": "Chỉ Đã dịch", + "Source Only": "Chỉ Nguồn", + "TTS Text": "Văn bản TTS" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 7413e594..4b132353 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "点击两侧翻页", "Click Both Sides": "点击两侧翻页", "Swap Tap Sides": "交换点击区域", - "Swap Click Sides": "交换点击区域" + "Swap Click Sides": "交换点击区域", + "Source and Translated": "原文和译文", + "Translated Only": "仅译文", + "Source Only": "仅原文", + "TTS Text": "TTS 文本" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index a21c4dcc..39be08fe 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -612,5 +612,9 @@ "Tap Both Sides": "點擊兩側翻頁", "Click Both Sides": "點擊兩側翻頁", "Swap Tap Sides": "交換點擊區域", - "Swap Click Sides": "交換點擊區域" + "Swap Click Sides": "交換點擊區域", + "Source and Translated": "原文和翻譯", + "Translated Only": "僅翻譯", + "Source Only": "僅原文", + "TTS Text": "TTS 文字" } diff --git a/apps/readest-app/src/app/reader/components/TranslationToggler.tsx b/apps/readest-app/src/app/reader/components/TranslationToggler.tsx index b6352c5f..6930a75d 100644 --- a/apps/readest-app/src/app/reader/components/TranslationToggler.tsx +++ b/apps/readest-app/src/app/reader/components/TranslationToggler.tsx @@ -46,7 +46,7 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => { } aria-label={_('Toggle Translation')} - disabled={!translationAvailable} + disabled={!translationAvailable && !translationEnabled} onClick={() => setTranslationEnabled(!translationEnabled)} label={ translationAvailable diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 19828bde..931875ee 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -18,6 +18,7 @@ import { Insets } from '@/types/misc'; import { Overlay } from '@/components/Overlay'; import { fetchImageAsBase64 } from '@/utils/image'; import { invokeUseBackgroundAudio } from '@/utils/bridge'; +import { getLocale } from '@/utils/misc'; import Popup from '@/components/Popup'; import TTSPanel from './TTSPanel'; import TTSIcon from './TTSIcon'; @@ -246,6 +247,27 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ttsController, bookKey]); + const getTTSTargetLang = useCallback((): string | null => { + const ttsReadAloudText = viewSettings?.ttsReadAloudText; + if (viewSettings?.translationEnabled && ttsReadAloudText === 'translated') { + return viewSettings?.translateTargetLang || getLocale(); + } else if (viewSettings?.translationEnabled && ttsReadAloudText === 'source') { + const bookData = getBookData(bookKey); + return bookData?.book?.primaryLanguage || ''; + } + return null; + }, [ + bookKey, + getBookData, + viewSettings?.translationEnabled, + viewSettings?.ttsReadAloudText, + viewSettings?.translateTargetLang, + ]); + + useEffect(() => { + ttsControllerRef.current?.setTargetLang(getTTSTargetLang() || ''); + }, [getTTSTargetLang]); + const handleTTSSpeak = async (event: CustomEvent) => { const { bookKey: ttsBookKey, range } = event.detail; if (bookKey !== ttsBookKey) return; @@ -311,6 +333,7 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => { ttsController.setLang(lang); ttsController.setRate(viewSettings.ttsRate); ttsController.speak(ssml); + ttsController.setTargetLang(getTTSTargetLang() || ''); ttsControllerRef.current = ttsController; setTtsController(ttsController); } diff --git a/apps/readest-app/src/components/settings/LangPanel.tsx b/apps/readest-app/src/components/settings/LangPanel.tsx index 04e3774b..d262fac4 100644 --- a/apps/readest-app/src/components/settings/LangPanel.tsx +++ b/apps/readest-app/src/components/settings/LangPanel.tsx @@ -26,6 +26,7 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider); const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang); const [showTranslateSource, setShowTranslateSource] = useState(viewSettings.showTranslateSource); + const [ttsReadAloudText, setTtsReadAloudText] = useState(viewSettings.ttsReadAloudText); const resetToDefaults = useResetViewSettings(); @@ -35,6 +36,8 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset translationEnabled: setTranslationEnabled, translationProvider: setTranslationProvider, translateTargetLang: setTranslateTargetLang, + showTranslateSource: setShowTranslateSource, + ttsReadAloudText: setTtsReadAloudText, }); }; @@ -114,6 +117,20 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset setViewSettings(bookKey, { ...viewSettings }); }; + const handleSelectTTSText = (event: React.ChangeEvent) => { + const option = event.target.value; + setTtsReadAloudText(option); + saveViewSettings(envConfig, bookKey, 'ttsReadAloudText', option, false, false); + }; + + const getTTSTextOptions = () => { + return [ + { value: 'both', label: _('Source and Translated') }, + { value: 'translated', label: _('Translated Only') }, + { value: 'source', label: _('Source Only') }, + ]; + }; + useEffect(() => { if (uiLanguage === viewSettings.uiLanguage) return; saveViewSettings(envConfig, bookKey, 'uiLanguage', uiLanguage, false, false); @@ -139,6 +156,12 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset // eslint-disable-next-line react-hooks/exhaustive-deps }, [showTranslateSource]); + useEffect(() => { + if (ttsReadAloudText === viewSettings.ttsReadAloudText) return; + saveViewSettings(envConfig, bookKey, 'ttsReadAloudText', ttsReadAloudText, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ttsReadAloudText]); + return (
@@ -177,18 +200,25 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset type='checkbox' className='toggle' checked={showTranslateSource} - disabled={!translationEnabled} onChange={() => setShowTranslateSource(!showTranslateSource)} />
+
+ {_('TTS Text')} +
@@ -198,7 +228,6 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset value={getCurrentTargetLangOption().value} onChange={handleSelectTargetLang} options={getLangOptions(TRANSLATOR_LANGS)} - disabled={!translationEnabled} />
diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index e7747504..4fc758c2 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -226,6 +226,7 @@ export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = { translationProvider: 'deepl', translateTargetLang: '', showTranslateSource: true, + ttsReadAloudText: 'both', }; export const DEFAULT_SCREEN_CONFIG: ScreenConfig = { diff --git a/apps/readest-app/src/services/tts/TTSController.ts b/apps/readest-app/src/services/tts/TTSController.ts index c227d4c8..1a50533b 100644 --- a/apps/readest-app/src/services/tts/TTSController.ts +++ b/apps/readest-app/src/services/tts/TTSController.ts @@ -1,6 +1,6 @@ import { FoliateView } from '@/types/view'; import { AppService } from '@/types/system'; -import { parseSSMLMarks } from '@/utils/ssml'; +import { filterSSMLWithLang, parseSSMLMarks } from '@/utils/ssml'; import { Overlayer } from 'foliate-js/overlayer.js'; import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types'; import { createRejectFilter } from '@/utils/node'; @@ -39,6 +39,7 @@ export class TTSController extends EventTarget { ttsWebVoices: TTSVoice[] = []; ttsEdgeVoices: TTSVoice[] = []; ttsNativeVoices: TTSVoice[] = []; + ttsTargetLang: string = ''; constructor(appService: AppService | null, view: FoliateView) { super(); @@ -141,6 +142,9 @@ export class TTSController extends EventTarget { .replace(/\*/g, ' ') .replace(/·/g, ' '); + if (this.ttsTargetLang) { + ssml = filterSSMLWithLang(ssml, this.ttsTargetLang); + } return ssml; } @@ -362,6 +366,10 @@ export class TTSController extends EventTarget { return this.ttsClient.getSpeakingLang(); } + setTargetLang(lang: string) { + this.ttsTargetLang = lang; + } + dispatchSpeakMark(mark?: TTSMark) { this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark || { text: '' } })); if (mark) { diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index 9fdfba11..08fa9dbb 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -182,6 +182,7 @@ export interface TranslatorConfig { translationProvider: string; translateTargetLang: string; showTranslateSource: boolean; + ttsReadAloudText: string; } export interface ScreenConfig { diff --git a/apps/readest-app/src/utils/reload.ts b/apps/readest-app/src/utils/reload.ts index ad2faf5b..32634531 100644 --- a/apps/readest-app/src/utils/reload.ts +++ b/apps/readest-app/src/utils/reload.ts @@ -1,7 +1,5 @@ import { RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants'; -import { eventDispatcher } from './event'; export const saveAndReload = async () => { - await eventDispatcher.dispatch('beforereload'); setTimeout(() => window.location.reload(), RELOAD_BEFORE_SAVED_TIMEOUT_MS); }; diff --git a/apps/readest-app/src/utils/ssml.ts b/apps/readest-app/src/utils/ssml.ts index a9ac74ff..f98b5639 100644 --- a/apps/readest-app/src/utils/ssml.ts +++ b/apps/readest-app/src/utils/ssml.ts @@ -116,3 +116,59 @@ export const findSSMLMark = (charIndex: number, marks: TTSMark[]) => { return result; }; + +export const filterSSMLWithLang = ( + ssml: string, + targetLang: string, + primaryLang?: string, +): string => { + const mainLang = parseSSMLLang(ssml, primaryLang); + + // Normalize target language + const normalizedTarget = code6392to6391(targetLang.toLowerCase()) || targetLang.toLowerCase(); + + // Check if target matches main language + if (isSameLang(normalizedTarget, mainLang)) { + // Remove all blocks that don't match the main language + return ssml.replace(/]*>.*?<\/lang>/gs, (match, langAttr) => { + const blockLang = code6392to6391(langAttr.toLowerCase()) || langAttr.toLowerCase(); + // If the lang block matches the main language, keep it as is + if (isSameLang(blockLang, mainLang)) { + return match; + } + // Otherwise remove the entire block + return ''; + }); + } + + // Check if target matches any block + const langBlocks: Array<{ match: string; lang: string; content: string }> = []; + const langBlockRegex = /]*>(.*?)<\/lang>/gs; + let match: RegExpExecArray | null; + + const tempRegex = new RegExp(langBlockRegex.source, langBlockRegex.flags); + while ((match = tempRegex.exec(ssml)) !== null) { + const blockLang = code6392to6391(match[1]!.toLowerCase()) || match[1]!.toLowerCase(); + if (isSameLang(blockLang, normalizedTarget)) { + langBlocks.push({ + match: match[0]!, + lang: match[1]!, + content: match[2]!, + }); + } + } + + if (langBlocks.length > 0) { + const speakOpenMatch = ssml.match(/]*>/i); + const speakCloseMatch = ssml.match(/<\/speak>/i); + + if (!speakOpenMatch || !speakCloseMatch) { + return ssml; + } + + const combinedContent = langBlocks.map((block) => block.match).join(''); + return `${speakOpenMatch[0]}${combinedContent}${speakCloseMatch[0]}`; + } + + return ssml; +};