From 9d3fc0786566ac2e683f26a4e8f7c666eb9f15fa Mon Sep 17 00:00:00 2001 From: Yurii Kostyukov Date: Mon, 21 Jul 2025 18:13:13 +0300 Subject: [PATCH] feat: Added Yandex Translator (#1652) --- README.md | 1 + .../src-tauri/capabilities/default.json | 3 + apps/readest-app/src-tauri/tauri.conf.json | 2 +- .../services/translators/providers/index.ts | 3 + .../services/translators/providers/yandex.ts | 77 +++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/services/translators/providers/yandex.ts diff --git a/README.md b/README.md index b10f5de6..e7c773a5 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ | **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ | | **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ | | **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ | +| **Translate with Yandex** | Instantly translate text or books using Yandex Translate. | ✅ | | **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ | | **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ | | **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ | diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index f88a0c9a..0cbd894f 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -68,6 +68,9 @@ { "url": "https://edge.microsoft.com" }, + { + "url": "https://translate.toil.cc" + }, { "url": "https://*.microsofttranslator.com" }, diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index 7f0df8ff..f7fe3073 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -15,7 +15,7 @@ "security": { "csp": { "default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost", - "connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com", + "connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com", "img-src": "'self' blob: data: asset: http://asset.localhost https://*", "style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://ik.imagekit.io", "font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://ik.imagekit.io", diff --git a/apps/readest-app/src/services/translators/providers/index.ts b/apps/readest-app/src/services/translators/providers/index.ts index 8aa96413..7f4b557c 100644 --- a/apps/readest-app/src/services/translators/providers/index.ts +++ b/apps/readest-app/src/services/translators/providers/index.ts @@ -2,6 +2,7 @@ import { TranslationProvider } from '../types'; import { deeplProvider } from './deepl'; import { azureProvider } from './azure'; import { googleProvider } from './google'; +import { yandexProvider } from './yandex'; function createTranslator( name: T, @@ -18,11 +19,13 @@ function createTranslator( const deeplTranslator = createTranslator('deepl', deeplProvider); const azureTranslator = createTranslator('azure', azureProvider); const googleTranslator = createTranslator('google', googleProvider); +const yandexTranslator = createTranslator('yandex', yandexProvider); const availableTranslators = [ deeplTranslator, azureTranslator, googleTranslator, + yandexTranslator, // Add more translators here ]; diff --git a/apps/readest-app/src/services/translators/providers/yandex.ts b/apps/readest-app/src/services/translators/providers/yandex.ts new file mode 100644 index 00000000..6c1810cf --- /dev/null +++ b/apps/readest-app/src/services/translators/providers/yandex.ts @@ -0,0 +1,77 @@ +import { stubTranslation as _ } from '@/utils/misc'; +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; +import { isTauriAppPlatform } from '@/services/environment'; +import { normalizeToShortLang } from '@/utils/lang'; +import { TranslationProvider } from '../types'; + +/** + * Based on https://translate.toil.cc/v2/docs API specification + */ +async function translateSingleTextForService( + text: string, + lang: string, + service: string, +): Promise { + const fetchImpl = isTauriAppPlatform() ? tauriFetch : window.fetch; + const url = 'https://translate.toil.cc/v2/translate/'; + + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + lang: lang, + service: service, + text: text, + }) + }; + + const response = await fetchImpl(url, request); + + if (!response.ok) { + const response_json = JSON.stringify(await response.json()); + throw new Error(`${service} failed with status ${response.status}\n${text.length}\n${JSON.stringify(request)}\n${response_json}`); + } + + const data = await response.json(); + if ( + data && + Array.isArray(data.translations) + ) { + return data.translations; + } else { + // fallback: return original texts if translation failed + return [text]; + } +}; + +export const yandexProvider: TranslationProvider = { + name: 'yandex', + label: _('Yandex Translate'), + authRequired: false, + translate: async (texts: string[], sourceLang: string, targetLang: string): Promise => { + if (!texts.length) return []; + + /** + Possible options: + - yandexcloud: often returns 500: {"error":"The text couldn't be translated, because Forbidden"} + - yandexgpt: often better than others + - yandextranslate + - yandexbrowser + */ + const service = "yandexgpt"; + + // Yandex does not accept "auto" language + const source_lang = sourceLang == "AUTO" ? "en" : normalizeToShortLang(sourceLang).toLowerCase(); + const target_lang = normalizeToShortLang(targetLang).toLowerCase(); + const lang = `${source_lang}-${target_lang}`; + + const responses = await Promise.all(texts.map(async text => { + return await translateSingleTextForService(text, lang, service) + })); + + const translatedTexts = responses.flat(); + return translatedTexts; + } +};