Last active
October 30, 2025 22:59
-
-
Save IntrovertedMage/6f17103b14fdc77849ad111315d4ec24 to your computer and use it in GitHub Desktop.
A KOReader user patch to use DeepLs api for translation instead of google translates api. Demonstration: https://imgur.com/a/zTG3EUn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --SETTINGS--======================================== | |
| local api_key = "ENTER API KEY HERE" | |
| local USE_FOR_TRANSLATION = true | |
| local USE_FOR_DETECTION = true | |
| local SHOW_DEEPL_LANGUAGES_AS_OPTIONS = true | |
| -------------========================= | |
| local Translator = require("ui/translator") | |
| local Device = require("device") | |
| local InfoMessage = require("ui/widget/infomessage") | |
| local TextViewer = require("ui/widget/textviewer") | |
| local UIManager = require("ui/uimanager") | |
| local Screen = require("device").screen | |
| local ffiUtil = require("ffi/util") | |
| local logger = require("logger") | |
| local T = ffiUtil.template | |
| local _ = require("gettext") | |
| local AUTODETECT_LANGUAGE = "auto" | |
| local DEEPL_SUPPORTED_SOURCE_LANGUAGES = { | |
| AR = "Arabic", | |
| BG = "Bulgarian", | |
| CS = "Czech", | |
| DA = "Danish", | |
| DE = "German", | |
| EL = "Greek", | |
| EN = "English (all English variants)", | |
| ES = "Spanish", | |
| ET = "Estonian", | |
| FI = "Finnish", | |
| FR = "French", | |
| HU = "Hungarian", | |
| ID = "Indonesian", | |
| IT = "Italian", | |
| JA = "Japanese", | |
| KO = "Korean", | |
| LT = "Lithuanian", | |
| LV = "Latvian", | |
| NB = "Norwegian Bokmål", | |
| NL = "Dutch", | |
| PL = "Polish", | |
| PT = "Portuguese (all Portuguese variants)", | |
| RO = "Romanian", | |
| RU = "Russian", | |
| SK = "Slovak", | |
| SL = "Slovenian", | |
| SV = "Swedish", | |
| TR = "Turkish", | |
| UK = "Ukrainian", | |
| ZH = "Chinese (all Chinese variants)" | |
| } | |
| local DEEPL_SUPPORTED_TARGET_LANGUAGES = { | |
| AR = "Arabic", | |
| BG = "Bulgarian", | |
| CS = "Czech", | |
| DA = "Danish", | |
| DE = "German", | |
| EL = "Greek", | |
| EN = "English (unspecified variant for backward compatibility; please select EN-GB or EN-US instead)", | |
| ["EN-GB"] = "English (British)", | |
| ["EN-US"] = "English (American)", | |
| ES = "Spanish", | |
| ET = "Estonian", | |
| FI = "Finnish", | |
| FR = "French", | |
| HU = "Hungarian", | |
| ID = "Indonesian", | |
| IT = "Italian", | |
| JA = "Japanese", | |
| KO = "Korean", | |
| LT = "Lithuanian", | |
| LV = "Latvian", | |
| NB = "Norwegian Bokmål", | |
| NL = "Dutch", | |
| PL = "Polish", | |
| PT = "Portuguese (unspecified variant for backward compatibility; please select PT-BR or PT-PT instead)", | |
| ["PT-BR"] = "Portuguese (Brazilian)", | |
| ["PT-PT"] = "Portuguese (all Portuguese variants excluding Brazilian Portuguese)", | |
| RO = "Romanian", | |
| RU = "Russian", | |
| SK = "Slovak", | |
| SL = "Slovenian", | |
| SV = "Swedish", | |
| TR = "Turkish", | |
| UK = "Ukrainian", | |
| ZH = "Chinese (unspecified variant for backward compatibility; please select ZH-HANS or ZH-HANT instead)", | |
| ["ZH-HANS"] = "Chinese (Simplified)", | |
| ["ZH-HANT"] = "Chinese (Traditional)" | |
| } | |
| local function loadDeepLPage(text, target_lang, source_lang) | |
| local http = require("socket.http") | |
| local ltn12 = require("ltn12") | |
| local json = require("json") | |
| local socketutil = require("socketutil") | |
| local socket = require("socket") | |
| local url = "https://api-free.deepl.com/v2/translate" | |
| -- request_body.target_lang = target_lang | |
| --request_body.source_lang = source_lang | |
| local request_body = { | |
| text = {text}, -- Wrap text in a table (array) for DeepL API | |
| target_lang = target_lang, | |
| } | |
| if source_lang and source_lang ~= "auto" then | |
| request_body.source_lang = source_lang | |
| end | |
| local request_json = json.encode(request_body) | |
| local response_body = {} | |
| socketutil:set_timeout() | |
| local request = { | |
| url = url, | |
| method = "POST", | |
| headers = { | |
| ["User-Agent"] = "KOReader", | |
| ["Host"] = "api-free.deepl.com", | |
| ["Accept"] = "application/json", | |
| ["Authorization"] = "DeepL-Auth-Key " .. api_key, | |
| ["Content-Type"] = "application/json", | |
| ["Content-Length"] = tostring(#request_json) -- Fix Content-Length | |
| }, | |
| source = ltn12.source.string(request_json), -- Send JSON | |
| sink = ltn12.sink.table(response_body) | |
| } | |
| logger.dbg("Calling", request.url) | |
| -- Skip first argument (body, goes to the sink) | |
| local res, code, headers, status = http.request(request) | |
| socketutil:reset_timeout() | |
| -- raise error message when network is unavailable | |
| if headers == nil then | |
| error(status or code or "network unreachable") | |
| end | |
| if code ~= 200 then | |
| logger.warn("translator HTTP status not okay:", status or code or "network unreachable") | |
| logger.dbg("Response headers:", headers) | |
| return | |
| end | |
| if table.concat(response_body) then | |
| local response_str = table.concat(response_body) | |
| local content = json.decode(response_str) | |
| logger.dbg("translator content:", content) | |
| if content.translations then | |
| return content.translations | |
| else | |
| logger.warn("translator error") | |
| end | |
| else | |
| logger.warn("not JSON in translator response:", response_body) | |
| end | |
| end | |
| local detect_original = Translator.detect | |
| Translator.detect = function(self,text) | |
| if not USE_FOR_DETECTION then | |
| return detect_original(self,text) | |
| end | |
| local result = loadDeepLPage(text, "en", AUTODETECT_LANGUAGE) | |
| if result and result[1] and result[1].detected_source_language then | |
| local src_lang = result.detected_source_language | |
| logger.dbg("detected language:", src_lang) | |
| return src_lang | |
| else | |
| return self.default_lang | |
| end | |
| end | |
| local translate_original = Translator.translate | |
| Translator.translate = function(self, text, target_lang, source_lang) | |
| if not USE_FOR_TRANSLATION then | |
| return translate_original(self, text, target_lang, source_lang) | |
| end | |
| if not target_lang then | |
| target_lang = self:getTargetLanguage() | |
| end | |
| if not source_lang then | |
| source_lang = self:getSourceLanguage() | |
| end | |
| if not DEEPL_SUPPORTED_TARGET_LANGUAGES[target_lang] then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Language: "..target_lang.." not supported as target language for translation by DeepL") | |
| }) | |
| return | |
| end | |
| if source_lang ~= "auto" and not DEEPL_SUPPORTED_SOURCE_LANGUAGES[source_lang] then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Language: "..target_lang.." not supported as source language for translation by DeepL") | |
| }) | |
| return | |
| end | |
| local result = loadDeepLPage(text, target_lang, source_lang) | |
| if result and result[1] and type(result[1]) == "table" then | |
| local translated = {} | |
| for i, r in ipairs(result) do | |
| table.insert(translated, r.text) | |
| end | |
| return table.concat(translated, "") | |
| end | |
| return nil | |
| end | |
| local _showTranslation_origianl = Translator._showTranslation | |
| Translator._showTranslation = function(self, text, detailed_view, source_lang, target_lang, from_highlight, index) | |
| if not USE_FOR_TRANSLATION then | |
| return _showTranslation_origianl(self, text, detailed_view, source_lang, target_lang, from_highlight, index) | |
| end | |
| if not target_lang then | |
| target_lang = self:getTargetLanguage() | |
| end | |
| if not source_lang then | |
| source_lang = self:getSourceLanguage() | |
| end | |
| if not DEEPL_SUPPORTED_TARGET_LANGUAGES[(string.upper(target_lang))] then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Language: "..target_lang.." not supported as target language for translation by DeepL") | |
| }) | |
| return | |
| end | |
| if source_lang ~= "auto" and not DEEPL_SUPPORTED_SOURCE_LANGUAGES[(string.upper(source_lang))] then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Language: "..target_lang.." not supported as source language for translation by DeepL") | |
| }) | |
| return | |
| end | |
| local Trapper = require("ui/trapper") | |
| local completed, result = Trapper:dismissableRunInSubprocess(function() | |
| return loadDeepLPage(text, target_lang, source_lang) | |
| end, _("Querying translation service…")) | |
| if not completed then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Translation interrupted.") | |
| }) | |
| return | |
| end | |
| if not result or type(result) ~= "table" then | |
| UIManager:show(InfoMessage:new{ | |
| text = _("Translation failed.") | |
| }) | |
| return | |
| end | |
| if result[1] and result[1].detected_source_language then | |
| source_lang = result[1].detected_source_language | |
| end | |
| local output = {} | |
| local text_main = "" | |
| local function is_result_valid(res) | |
| return res and type(res) == "table" and #res > 0 | |
| end | |
| -- For both main and alternate translations, we may get multiple slices | |
| -- of the original text and its translations. | |
| if is_result_valid(result) then | |
| -- Main translation: we can make a single string from the multiple parts | |
| -- for easier quick reading | |
| local source = {} | |
| local translated = {} | |
| local romanized = {} | |
| for i, r in ipairs(result) do | |
| if detailed_view then | |
| local s = type(text) == "string" and text or "" | |
| table.insert(source, s) | |
| if type(r.romanized) == "string" then | |
| table.insert(romanized, r.romanized) | |
| end | |
| end | |
| local t = type(r.text) == "string" and r.text or "" | |
| table.insert(translated, t) | |
| end | |
| text_main = table.concat(translated, " ") | |
| if detailed_view then | |
| text_main = "● " .. text_main | |
| table.insert(output, "▣ " .. table.concat(source, " ")) | |
| if #romanized > 0 then | |
| table.insert(output, table.concat(romanized, " ")) | |
| end | |
| end | |
| table.insert(output, text_main) | |
| end | |
| -- table.insert(output, require("dump")(result)) -- for debugging | |
| local text_all = table.concat(output, "\n") | |
| local textviewer, height, buttons_table, close_callback | |
| if detailed_view then | |
| height = math.floor(Screen:getHeight() * 0.8) | |
| buttons_table = {} | |
| if from_highlight then | |
| local ui = require("apps/reader/readerui").instance | |
| table.insert(buttons_table, | |
| { | |
| { | |
| text = _("Save main translation to note"), | |
| callback = function() | |
| UIManager:close(textviewer) | |
| UIManager:close(ui.highlight.highlight_dialog) | |
| ui.highlight.highlight_dialog = nil | |
| if index then | |
| ui.highlight:editNote(index, false, text_main) | |
| else | |
| ui.highlight:addNote(text_main) | |
| end | |
| end, | |
| }, | |
| { | |
| text = _("Save all to note"), | |
| callback = function() | |
| UIManager:close(textviewer) | |
| UIManager:close(ui.highlight.highlight_dialog) | |
| ui.highlight.highlight_dialog = nil | |
| if index then | |
| ui.highlight:editNote(index, false, text_all) | |
| else | |
| ui.highlight:addNote(text_all) | |
| end | |
| end, | |
| }, | |
| } | |
| ) | |
| close_callback = function() | |
| if not ui.highlight.highlight_dialog then | |
| ui.highlight:clear() | |
| end | |
| end | |
| end | |
| if Device:hasClipboard() then | |
| table.insert(buttons_table, | |
| { | |
| { | |
| text = _("Copy main translation"), | |
| callback = function() | |
| Device.input.setClipboardText(text_main) | |
| end, | |
| }, | |
| { | |
| text = _("Copy all"), | |
| callback = function() | |
| Device.input.setClipboardText(text_all) | |
| end, | |
| }, | |
| } | |
| ) | |
| end | |
| end | |
| textviewer = TextViewer:new{ | |
| title = T(_("Translation from %1"), self:getLanguageName(source_lang, "?")), | |
| title_multilines = true, | |
| -- Showing the translation target language in this title may make | |
| -- it quite long and wrapped, taking valuable vertical spacing | |
| text = text_all, | |
| text_type = "lookup", | |
| height = height, | |
| add_default_buttons = true, | |
| buttons_table = buttons_table, | |
| close_callback = close_callback, | |
| } | |
| UIManager:show(textviewer) | |
| end | |
| local genSettingsMenu_original = Translator.genSettingsMenu | |
| Translator.genSettingsMenu = function (self) | |
| local menu = genSettingsMenu_original(self) | |
| if not SHOW_DEEPL_LANGUAGES_AS_OPTIONS then | |
| return menu | |
| end | |
| local function genLanguagesItems(setting_name, default_checked_item) | |
| local items_table = {} | |
| for lang_key, lang_name in ffiUtil.orderedPairs(DEEPL_SUPPORTED_TARGET_LANGUAGES) do | |
| table.insert(items_table, { | |
| text_func = function() | |
| return T("%1 (%2)", lang_name, lang_key) | |
| end, | |
| checked_func = function() | |
| return lang_key == (G_reader_settings:readSetting(setting_name) or default_checked_item) | |
| end, | |
| callback = function() | |
| G_reader_settings:saveSetting(setting_name, lang_key) | |
| end, | |
| }) | |
| end | |
| return items_table | |
| end | |
| menu.sub_item_table[#menu.sub_item_table].sub_item_table = genLanguagesItems("translator_to_language", self:getTargetLanguage()) | |
| table.insert(menu.sub_item_table, { | |
| text = "Currently using DeepL API for translations" | |
| }) | |
| return menu | |
| end | |
Thank you!
On my Android device (Boox Go Color 7), it translates fine, but KOReader instantly freezes as soon as the result comes up. I have to force quit it to proceed reading. Any ideas why?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Worked it like a charm, thank you for the update!