Skip to content

Instantly share code, notes, and snippets.

@IntrovertedMage
Last active October 30, 2025 22:59
Show Gist options
  • Save IntrovertedMage/6f17103b14fdc77849ad111315d4ec24 to your computer and use it in GitHub Desktop.
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
--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
@komadorirobin
Copy link

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