--[[--
This module translates text using Google Translate.

<https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=fr&dt=t&q=alea%20jacta%20est>

--]]

-- Useful other implementation and discussion:
--  https://github.com/ssut/py-googletrans/blob/master/googletrans/client.py
--  https://stackoverflow.com/questions/26714426/what-is-the-meaning-of-google-translate-query-params

local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local TextViewer = require("ui/widget/textviewer")
local UIManager = require("ui/uimanager")
local JSON = require("json")
local Screen = require("device").screen
local ffiUtil  = require("ffi/util")
local logger = require("logger")
local util = require("util")
local T = ffiUtil.template
local _ = require("gettext")

-- Extracted from https://translate.google.com/
-- 20251003: 249 supported languages
local AUTODETECT_LANGUAGE = "auto"
local SUPPORTED_LANGUAGES = {
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    ab = _("Abkhaz"),
    ace = _("Acehnese"),
    ach = _("Acholi"),
    aa = _("Afar"),
    af = _("Afrikaans"),
    sq = _("Albanian"),
    alz = _("Alur"),
    am = _("Amharic"),
    ar = _("Arabic"),
    hy = _("Armenian"),
    as = _("Assamese"),
    av = _("Avar"),
    awa = _("Awadhi"),
    ay = _("Aymara"),
    az = _("Azerbaijani"),
    ban = _("Balinese"),
    bal = _("Baluchi"),
    bm = _("Bambara"),
    bci = _("Baoulé"),
    ba = _("Bashkir"),
    eu = _("Basque"),
    btx = _("Batak Karo"),
    bts = _("Batak Simalungun"),
    bbc = _("Batak Toba"),
    be = _("Belarusian"),
    bem = _("Bemba"),
    bn = _("Bengali"),
    bew = _("Betawi"),
    bho = _("Bhojpuri"),
    bik = _("Bikol"),
    bs = _("Bosnian"),
    br = _("Breton"),
    bg = _("Bulgarian"),
    bua = _("Buryat"),
    yue = _("Cantonese"),
    ca = _("Catalan"),
    ceb = _("Cebuano"),
    ch = _("Chamorro"),
    ce = _("Chechen"),
    ny = _("Chichewa"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    zh = _("Chinese (Simplified)"), -- "Simplified Chinese may be specified either by zh-CN or zh"
    zh_TW = _("Chinese (Traditional)"), -- converted to "zh-TW" below
    chk = _("Chuukese"),
    cv = _("Chuvash"),
    co = _("Corsican"),
    crh = _("Crimean Tatar (Cyrillic)"),
    ["crh-Latn"] = _("Crimean Tatar (Latin)"),
    hr = _("Croatian"),
    cs = _("Czech"),
    da = _("Danish"),
    ["fa-AF"] = _("Dari"),
    dv = _("Dhivehi"),
    din = _("Dinka"),
    doi = _("Dogri"),
    dov = _("Dombe"),
    nl = _("Dutch"),
    dyu = _("Dyula"),
    dz = _("Dzongkha"),
    en = _("English"),
    eo = _("Esperanto"),
    et = _("Estonian"),
    ee = _("Ewe"),
    fo = _("Faroese"),
    fj = _("Fijian"),
    tl = _("Filipino"),
    fi = _("Finnish"),
    fon = _("Fon"),
    fr = _("French"),
    ["fr-CA"] = _("French (Canada)"),
    fy = _("Frisian"),
    fur = _("Friulian"),
    ff = _("Fulani"),
    gaa = _("Ga"),
    gl = _("Galician"),
    ka = _("Georgian"),
    de = _("German"),
    el = _("Greek"),
    gn = _("Guarani"),
    gu = _("Gujarati"),
    ht = _("Haitian Creole"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    cnh = _("Hakha Chin"),
    ha = _("Hausa"),
    haw = _("Hawaiian"),
    he = _("Hebrew"), -- "Hebrew may be specified either by he or iw"
    hil = _("Hiligaynon"),
    hi = _("Hindi"),
    hmn = _("Hmong"),
    hu = _("Hungarian"),
    hrx = _("Hunsrik"),
    iba = _("Iban"),
    is = _("Icelandic"),
    ig = _("Igbo"),
    ilo = _("Ilocano"),
    id = _("Indonesian"),
    ["iu-Latn"] = _("Inuktut (Latin)"),
    iu = _("Inuktut (Syllabics)"),
    ga = _("Irish"),
    it = _("Italian"),
    jam = _("Jamaican Patois"),
    ja = _("Japanese"),
    jw = _("Javanese"),
    kac = _("Jingpo"),
    kl = _("Kalaallisut"),
    kn = _("Kannada"),
    kr = _("Kanuri"),
    pam = _("Kapampangan"),
    kk = _("Kazakh"),
    kha = _("Khasi"),
    km = _("Khmer"),
    cgg = _("Kiga"),
    kg = _("Kikongo"),
    rw = _("Kinyarwanda"),
    ktu = _("Kituba"),
    trp = _("Kokborok"),
    kv = _("Komi"),
    gom = _("Konkani"),
    ko = _("Korean"),
    kri = _("Krio"),
    ku = _("Kurdish (Kurmanji)"),
    ckb = _("Kurdish (Sorani)"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    ky = _("Kyrgyz"),
    lo = _("Lao"),
    ltg = _("Latgalian"),
    la = _("Latin"),
    lv = _("Latvian"),
    lij = _("Ligurian"),
    li = _("Limburgish"),
    ln = _("Lingala"),
    lt = _("Lithuanian"),
    lmo = _("Lombard"),
    lg = _("Luganda"),
    luo = _("Luo"),
    lb = _("Luxembourgish"),
    mk = _("Macedonian"),
    mad = _("Madurese"),
    mai = _("Maithili"),
    mak = _("Makassar"),
    mg = _("Malagasy"),
    ms = _("Malay"),
    ["ms-Arab"] = _("Malay (Jawi)"),
    ml = _("Malayalam"),
    mt = _("Maltese"),
    mam = _("Mam"),
    gv = _("Manx"),
    mi = _("Maori"),
    mr = _("Marathi"),
    mh = _("Marshallese"),
    mwr = _("Marwadi"),
    mfe = _("Mauritian Creole"),
    chm = _("Meadow Mari"),
    ["mni-Mtei"] = _("Meiteilon (Manipuri)"),
    min = _("Minang"),
    lus = _("Mizo"),
    mn = _("Mongolian"),
    my = _("Myanmar (Burmese)"),
    ["bm-Nkoo"] = _("NKo"),
    nhe = _("Nahuatl (Eastern Huasteca)"),
    ["ndc-ZW"] = _("Ndau"),
    nr = _("Ndebele (South)"),
    new = _("Nepalbhasa (Newari)"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    ne = _("Nepali"),
    no = _("Norwegian"),
    nus = _("Nuer"),
    oc = _("Occitan"),
    ["or"] = _("Odia (Oriya)"),
    om = _("Oromo"),
    os = _("Ossetian"),
    pag = _("Pangasinan"),
    pap = _("Papiamento"),
    ps = _("Pashto"),
    fa = _("Persian"),
    pl = _("Polish"),
    pt = _("Portuguese (Brazil)"),
    ["pt-PT"] = _("Portuguese (Portugal)"),
    pa = _("Punjabi (Gurmukhi)"),
    ["pa-Arab"] = _("Punjabi (Shahmukhi)"),
    qu = _("Quechua"),
    kek = _("Qʼeqchiʼ"),
    rom = _("Romani"),
    ro = _("Romanian"),
    rn = _("Rundi"),
    ru = _("Russian"),
    se = _("Sami (North)"),
    sm = _("Samoan"),
    sg = _("Sango"),
    sa = _("Sanskrit"),
    ["sat-Latn"] = _("Santali (Latin)"),
    sat = _("Santali (Ol Chiki)"),
    gd = _("Scots Gaelic"),
    nso = _("Sepedi"),
    sr = _("Serbian"),
    st = _("Sesotho"),
    crs = _("Seychellois Creole"),
    shn = _("Shan"),
    sn = _("Shona"),
    scn = _("Sicilian"),
    szl = _("Silesian"),
    sd = _("Sindhi"),
    si = _("Sinhala"),
    sk = _("Slovak"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    sl = _("Slovenian"),
    so = _("Somali"),
    es = _("Spanish"),
    su = _("Sundanese"),
    sus = _("Susu"),
    sw = _("Swahili"),
    ss = _("Swati"),
    sv = _("Swedish"),
    ty = _("Tahitian"),
    tg = _("Tajik"),
    ["ber-Latn"] = _("Tamazight"),
    ber = _("Tamazight (Tifinagh)"),
    ta = _("Tamil"),
    tt = _("Tatar"),
    te = _("Telugu"),
    tet = _("Tetum"),
    th = _("Thai"),
    bo = _("Tibetan"),
    ti = _("Tigrinya"),
    tiv = _("Tiv"),
    tpi = _("Tok Pisin"),
    to = _("Tongan"),
    lua = _("Tshiluba"),
    ts = _("Tsonga"),
    tn = _("Tswana"),
    tcy = _("Tulu"),
    tum = _("Tumbuka"),
    tr = _("Turkish"),
    tk = _("Turkmen"),
    tyv = _("Tuvan"),
    ak = _("Twi"),
    udm = _("Udmurt"),
    uk = _("Ukrainian"),
    ur = _("Urdu"),
    ug = _("Uyghur"),
    uz = _("Uzbek"),
    ve = _("Venda"),
    vec = _("Venetian"),
    vi = _("Vietnamese"),
    war = _("Waray"),
    -- @translators Many of the names for languages can be conveniently found pre-translated in the relevant language of this Wikipedia article: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
    cy = _("Welsh"),
    wo = _("Wolof"),
    xh = _("Xhosa"),
    sah = _("Yakut"),
    yi = _("Yiddish"),
    yo = _("Yoruba"),
    yua = _("Yucatec Maya"),
    zap = _("Zapotec"),
    zu = _("Zulu"),
}

-- Fix zh_TW => zh-TW:
SUPPORTED_LANGUAGES["zh-TW"] = SUPPORTED_LANGUAGES["zh_TW"]
SUPPORTED_LANGUAGES["zh_TW"] = nil

local ALT_LANGUAGE_CODES = {}
ALT_LANGUAGE_CODES["zh-CN"] = "zh"
ALT_LANGUAGE_CODES["iw"] = "he"
ALT_LANGUAGE_CODES["tl"] = "fil"

local Translator = {
    trans_servers = {
        "https://translate.googleapis.com/",
        -- "http://translate.google.cn",
    },
    trans_path = "/translate_a/single",
    trans_params = {
        client = "gtx", -- (using "t" raises 403 Forbidden)
        ie = "UTF-8", -- input encoding
        oe = "UTF-8", -- output encoding
        sl = "auto",  -- source language (we need to specify "auto" to detect language)
        tl = "en", -- target language
        hl = "en", -- ?
        otf = 1,   -- ?
        ssel = 0,  -- ?
        tsel = 0,  -- ?
        -- tk = "" -- auth token
        dt = { -- what we want in result
            "t",   -- translation of source text
            "at",  -- alternate translations
            -- Next options only give additional results when text is a single word
            -- "bd",  -- dictionary (articles, reverse translations, etc)
            -- "ex",  -- examples
            -- "ld",  -- ?
            "md",  -- definitions of source text
            -- "qca", -- ?
            -- "rw",  -- "see also" list
            -- "rm",  -- transcription / transliteration of source and translated texts
            -- "ss",  -- synonyms of source text, if it's one word
        }
        -- q = text to translate
    },
    default_lang = "en",
}

function Translator:getTransServer()
    return G_reader_settings:readSetting("trans_server") or self.trans_servers[1]
end

function Translator:getLanguageName(lang, default_string)
    if SUPPORTED_LANGUAGES[lang] then
        return SUPPORTED_LANGUAGES[lang], true
    elseif ALT_LANGUAGE_CODES[lang] then
        return SUPPORTED_LANGUAGES[ALT_LANGUAGE_CODES[lang]], true
    elseif lang then
        return lang:upper(), false
    end
    return default_string, false
end

-- Will be called by ReaderHighlight to make it available in Reader menu
function Translator:genSettingsMenu()
    local function genLanguagesItems(setting_name, default_checked_item)
        local items_table = {}
        for lang_key, lang_name in ffiUtil.orderedPairs(SUPPORTED_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,
                radio = true,
                callback = function()
                    G_reader_settings:saveSetting(setting_name, lang_key)
                end,
            })
        end
        return items_table
    end
    return {
        text = _("Translation settings"),
        sub_item_table = {
            {
                text_func = function()
                    local __, name = self:getDocumentLanguage()
                    return T(_("Translate from book language: %1"), name or _("N/A"))
                end,
                help_text = _([[
With books that specify their main language in their metadata (most EPUBs and FB2s), enabling this option will make this language the source language. Otherwise, auto-detection or the selected language will be used.
This is useful:
- For books in a foreign language, where consistent translation is needed and words in other languages are rare.
- For books in familiar languages, to get definitions for words from the translation service.]]),
                enabled_func = function()
                    return self:getDocumentLanguage() ~= nil
                end,
                checked_func = function()
                    return G_reader_settings:isTrue("translator_from_doc_lang")
                end,
                callback = function()
                    G_reader_settings:flipTrue("translator_from_doc_lang")
                end,
            },
            {
                text = _("Auto-detect source language"),
                help_text = _("This setting is best suited for foreign text found in books written in your native language."),
                enabled_func = function()
                    return not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil)
                end,
                checked_func = function()
                    return G_reader_settings:nilOrTrue("translator_from_auto_detect")
                        and not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil)
                end,
                callback = function()
                    G_reader_settings:flipNilOrTrue("translator_from_auto_detect")
                end,
            },
            {
                text = _("Show romanizations"),
                help_text = _("Displays source language text in Latin characters. This is useful for reading languages with non-Latin scripts."),
                checked_func = function()
                    return G_reader_settings:isTrue("translator_with_romanizations")
                end,
                callback = function()
                    G_reader_settings:flipTrue("translator_with_romanizations")
                end,
            },
            {
                text_func = function()
                    local lang = G_reader_settings:readSetting("translator_from_language")
                    return T(_("Translate from: %1"), self:getLanguageName(lang, ""))
                end,
                help_text = _("If a specific source language is manually selected, it will be used everywhere, in all your books."),
                enabled_func = function()
                    return not G_reader_settings:nilOrTrue("translator_from_auto_detect")
                        and not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil)
                end,
                sub_item_table = genLanguagesItems("translator_from_language"),
                separator = true,
            },
            {
                text_func = function()
                    local lang = self:getTargetLanguage()
                    return T(_("Translate to: %1"), self:getLanguageName(lang, ""))
                end,
                sub_item_table = genLanguagesItems("translator_to_language", self:getTargetLanguage()),
            },
        },
    }
end

function Translator:getDocumentLanguage()
    local ui = require("apps/reader/readerui").instance
    local lang = ui and ui.doc_props and ui.doc_props.language
    if not lang then
        return
    end
    lang = lang:match("(.*)-") or lang
    lang = lang:lower()
    local name, supported = self:getLanguageName(lang, "")
    if supported then
        return lang, name
    end
    -- ReaderTypography has a map of lang aliases (that we may meet
    -- in book metadata) to their normalized lang tag: use it
    local ReaderTypography = require("apps/reader/modules/readertypography")
    lang = ReaderTypography.LANG_ALIAS_TO_LANG_TAG[lang]
    if not lang then
        return
    end
    name, supported = self:getLanguageName(lang, "")
    if supported then
        return lang, name
    end
end

function Translator:getSourceLanguage()
    if G_reader_settings:isTrue("translator_from_doc_lang") then
        local lang = self:getDocumentLanguage()
        if lang then
            return lang
        end
        -- No document or metadata lang tag not supported:
        -- fallback to other settings
    end
    if G_reader_settings:isFalse("translator_from_auto_detect") and
            G_reader_settings:has("translator_from_language") then
        return G_reader_settings:readSetting("translator_from_language")
    end
    return AUTODETECT_LANGUAGE -- "auto"
end

function Translator:getTargetLanguage()
    local lang = G_reader_settings:readSetting("translator_to_language")
    if not lang then
        -- Fallback to the UI language the user has selected
        lang = G_reader_settings:readSetting("language")
        if lang and lang ~= "" then
            -- convert "zh-CN" and "zh-TW" to "zh"
            lang = lang:match("(.*)-") or lang
            if lang == "C" then
                lang="en"
            end
            lang = lang:lower()
        end
    end
    return lang or "en"
end

--[[--
Returns decoded JSON table from translate server.

@string text
@string target_lang
@string source_lang
@treturn string result, or nil
--]]
function Translator:loadPage(text, target_lang, source_lang)
    local socket = require("socket")
    local socketutil = require("socketutil")
    local url = require("socket.url")
    local http = require("socket.http")
    local ltn12 = require("ltn12")

    local query = ""
    self.trans_params.tl = target_lang
    self.trans_params.sl = source_lang

    for k,v in pairs(self.trans_params) do
        if type(v) == "table" then
            for _, v2 in ipairs(v) do
                query = query .. k .. '=' .. v2 .. '&'
            end
        else
            query = query .. k .. '=' .. v .. '&'
        end
    end
    if G_reader_settings:isTrue("translator_with_romanizations") then
       query = query .. "dt=rm&"
    end
    local parsed = url.parse(self:getTransServer())
    parsed.path = self.trans_path
    parsed.query = query .. "q=" .. url.escape(text)

    -- HTTP request
    local sink = {}
    socketutil:set_timeout()
    local request = {
        url     = url.build(parsed),
        method  = "GET",
        sink    = ltn12.sink.table(sink),
    }
    logger.dbg("Calling", request.url)
    -- Skip first argument (body, goes to the sink)
    local code, headers, status = socket.skip(1, 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

    local content = table.concat(sink)
    -- logger.dbg("translator content:", content)
    local first_char = content:sub(1, 1)
    if content ~= "" and (first_char == "{" or first_char == "[") then
        -- Get nil instead of functions for 'null' by using JSON.decode.simple
        -- (so the result can be fully serialized when used
        -- with Trapper:dismissableRunInSubprocess())
        local ok, result = pcall(JSON.decode, content, JSON.decode.simple)
        if ok and result then
            logger.dbg("translator json:", result)
            return result
        else
            logger.warn("translator error:", result)
        end
    else
        logger.warn("not JSON in translator response:", content)
    end
end
-- The JSON result is a list of 9 to 15 items:
--    1: translation
--    2: all-translations
--    3: original-language
--    6: possible-translations
--    7: confidence
--    8: possible-mistakes
--    9: language
--   12: synonyms
--   13: definitions
--   14: examples
--   15: see-also
-- Depending on the 'dt' parameters used, some may be null or absent.
-- See bottom of this file for some sample results.

--[[--
Tries to automatically detect language of `text`.

@string text
@treturn string lang (`"en"`, `"fr"`, `…`)
--]]
function Translator:detect(text)
    local result = self:loadPage(text, "en", AUTODETECT_LANGUAGE)
    if result and result[3] then
        local src_lang = result[3]
        logger.dbg("detected language:", src_lang)
        return src_lang
    else
        return self.default_lang
    end
end

--[[--
Translate text, returns translation as a single string.

@string text
@string target_lang[opt] (`"en"`, `"fr"`, `…`)
@string source_lang[opt="auto"] (`"en"`, `"fr"`, `…`) or `"auto"` to auto-detect source language
@treturn string translated text, or nil
--]]
function Translator:translate(text, target_lang, source_lang)
    if not target_lang then
        target_lang = self:getTargetLanguage()
    end
    if not source_lang then
        source_lang = self:getSourceLanguage()
    end
    local result = self:loadPage(text, target_lang, source_lang)
    if result and result[1] and type(result[1]) == "table" then
        local translated = {}
        for i, r in ipairs(result[1]) do
            table.insert(translated, r[1])
        end
        return table.concat(translated, "")
    end
    return nil
end

--[[--
Show translated text in TextViewer, with alternate translations

@string text
@bool detailed_view "true" to show alternate translation, definition, additional buttons
@string source_lang[opt="auto"] (`"en"`, `"fr"`, `…`) or `"auto"` to auto-detect source language
@string target_lang[opt] (`"en"`, `"fr"`, `…`)
--]]
function Translator:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index)
    if Device:hasClipboard() then
        Device.input.setClipboardText(text)
    end

    local NetworkMgr = require("ui/network/manager")
    if NetworkMgr:willRerunWhenOnline(function()
                self:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index)
            end) then
        return
    end

    -- Wrap next function with Trapper to be able to interrupt
    -- translation service query.
    local Trapper = require("ui/trapper")
    Trapper:wrap(function()
        self:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index)
    end)
end

function Translator:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index)
    if not target_lang then
        target_lang = self:getTargetLanguage()
    end
    if not source_lang then
        source_lang = self:getSourceLanguage()
    end

    local Trapper = require("ui/trapper")
    local completed, result = Trapper:dismissableRunInSubprocess(function()
        return self:loadPage(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[3] then
        source_lang = result[3]
    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[1]) 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[1]) do
            if detailed_view then
                local s = type(r[2]) == "string" and r[2] or ""
                table.insert(source, s)
                if type(r[4]) == "string" then
                    table.insert(romanized, r[4])
                end
            end
            local t = type(r[1]) == "string" and r[1] 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

    if detailed_view then
        if is_result_valid(result[6]) then
            -- Alternative translations:
            table.insert(output, "")
            table.insert(output, _("Alternate translations:"))
            for i, r in ipairs(result[6]) do
                if type(r[3]) == "table" then
                    local s = type(r[1]) == "string" and r[1]:gsub("\n", "") or ""
                    table.insert(output, "▣ " .. s)
                    for j, rt in ipairs(r[3]) do
                        -- Use number in solid black circle symbol (U+2776...277F)
                        local symbol = util.unicodeCodepointToUtf8(10101 + (j < 10 and j or 10))
                        local t = type(rt[1]) == "string" and rt[1]:gsub("\n", "") or ""
                        table.insert(output, symbol .. " " .. t)
                    end
                end
            end
        end
        if is_result_valid(result[13]) then
            -- Definition(word)
            table.insert(output, "")
            table.insert(output, _("Definition:"))
            for i, r in ipairs(result[13]) do
                if r[2] and type(r[2]) == "table" then
                    local symbol = util.unicodeCodepointToUtf8(10101 + (i < 10 and i or 10))
                    table.insert(output, symbol.. " ".. r[1])
                    for j, res in ipairs(r[2]) do
                        table.insert(output, "\t● ".. res[1])
                    end
                end
            end
        end
    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

return Translator

-- Sample JSON results:
--
-- Multiple words result:
-- {
--     [1] = {
--         [1] = {
--             [1] = "I know you did not destroy your King's house, because then you had none. ",
--             [2] = "Ich weiß, dass ihr nicht eures Königs Haus zerstört habt, denn damals hattet ihr ja keinen.",
--             [5] = 3,
--             ["n"] = 5
--         },
--         [2] = {
--             [1] = "But you can not deny that you destroyed a royal palace. ",
--             [2] = "Aber ihr könnt nicht leugnen, dass ihr einen Königspalast zerstört habt.",
--             [5] = 3,
--             ["n"] = 5
--         },
--         [3] = {
--             [1] = "If the king is dead, then the kingdom remains, just as a ship remains, whose helmsman has fallen",
--             [2] = "Ist der König tot, so bleibt doch das Reich bestehen, ebenso wie ein Schiff bleibt, dessen Steuermann gefallen ist",
--             [5] = 3,
--             ["n"] = 5
--         }
--     },
--     [3] = "de",
--     [6] = {
--         [1] = {
--             [1] = "Ich weiß, dass ihr nicht eures Königs Haus zerstört habt, denn damals hattet ihr ja keinen.",
--             [3] = {
--                 [1] = {
--                     [1] = "I know you did not destroy your King's house, because then you had none.",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [2] = {
--                     [1] = "I know that you have not destroyed your king house, because at that time you had not any.",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 }
--             },
--             [4] = {
--                 [1] = {
--                     [1] = 0,
--                     [2] = 91
--                 }
--             },
--             [5] = "Ich weiß, dass ihr nicht eures Königs Haus zerstört habt, denn damals hattet ihr ja keinen.",
--             [6] = 0,
--             [7] = 0
--         },
--         [2] = {
--             [1] = "Aber ihr könnt nicht leugnen, dass ihr einen Königspalast zerstört habt.",
--             [3] = {
--                 [1] = {
--                     [1] = "But you can not deny that you destroyed a royal palace.",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [2] = {
--                     [1] = "But you can not deny that you have destroyed a royal palace.",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 }
--             },
--             [4] = {
--                 [1] = {
--                     [1] = 0,
--                     [2] = 72
--                 }
--             },
--             [5] = "Aber ihr könnt nicht leugnen, dass ihr einen Königspalast zerstört habt.",
--             [6] = 0,
--             [7] = 0
--         },
--         [3] = {
--             [1] = "Ist der König tot, so bleibt doch das Reich bestehen, ebenso wie ein Schiff bleibt, dessen Steuermann gefallen ist",
--             [3] = {
--                 [1] = {
--                     [1] = "If the king is dead, then the kingdom remains, just as a ship remains, whose helmsman has fallen",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [2] = {
--                     [1] = "yet the king dead, remains the kingdom stand remains as a ship the helmsman has fallen",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 }
--             },
--             [4] = {
--                 [1] = {
--                     [1] = 0,
--                     [2] = 114
--                 }
--             },
--             [5] = "Ist der König tot, so bleibt doch das Reich bestehen, ebenso wie ein Schiff bleibt, dessen Steuermann gefallen ist",
--             [6] = 0,
--             [7] = 0
--         }
--     },
--     [7] = 1,
--     [9] = {
--         [1] = {
--             [1] = "de"
--         },
--         [3] = {
--             [1] = 1
--         },
--         [4] = {
--             [1] = "de"
--         }
--     },
--     ["n"] = 9
-- }
--
-- Single word result with all dt= enabled:
-- {
--     [1] = {
--         [1] = {
--             [1] = "fork",
--             [2] = "fourchette",
--             [5] = 0,
--             ["n"] = 5
--         }
--     },
--     [2] = {
--         [1] = {
--             [1] = "noun",
--             [2] = {
--                 [1] = "fork"
--             },
--             [3] = {
--                 [1] = {
--                     [1] = "fork",
--                     [2] = {
--                         [1] = "fourche",
--                         [2] = "fourchette",
--                         [3] = "embranchement",
--                         [4] = "chariot",
--                         [5] = "chariot à fourche"
--                     },
--                     [4] = 0.21967085
--                 }
--             },
--             [4] = "fourchette",
--             [5] = 1
--         }
--     },
--     [3] = "fr",
--     [6] = {
--         [1] = {
--             [1] = "fourchette",
--             [3] = {
--                 [1] = {
--                     [1] = "fork",
--                     [2] = 1000,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [2] = {
--                     [1] = "band",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [3] = {
--                     [1] = "bracket",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 },
--                 [4] = {
--                     [1] = "range",
--                     [2] = 0,
--                     [3] = true,
--                     [4] = false
--                 }
--             },
--             [4] = {
--                 [1] = {
--                     [1] = 0,
--                     [2] = 10
--                 }
--             },
--             [5] = "fourchette",
--             [6] = 0,
--             [7] = 1
--         }
--     },
--     [7] = 1,
--     [9] = {
--         [1] = {
--             [1] = "fr"
--         },
--         [3] = {
--             [1] = 1
--         },
--         [4] = {
--             [1] = "fr"
--         }
--     },
--     [12] = {
--         [1] = {
--             [1] = "noun",
--             [2] = {
--                 [1] = {
--                     [1] = {
--                         [1] = "ramification",
--                         [2] = "enfourchure"
--                     },
--                     [2] = ""
--                 },
--                 [2] = {
--                     [1] = {
--                         [1] = "échéance",
--                         [2] = "bande"
--                     },
--                     [2] = ""
--                 },
--                 [3] = {
--                     [1] = {
--                         [1] = "ramification",
--                         [2] = "jambe"
--                     },
--                     [2] = ""
--                 },
--                 [4] = {
--                     [1] = {
--                         [1] = "bifurcation"
--                     },
--                     [2] = ""
--                 },
--                 [5] = {
--                     [1] = {
--                         [1] = "fourche",
--                         [2] = "bifurcation",
--                         [3] = "entrejambe"
--                     },
--                     [2] = ""
--                 },
--                 [6] = {
--                     [1] = {
--                         [1] = "fourche",
--                         [2] = "bifurcation"
--                     },
--                     [2] = ""
--                 }
--             },
--             [3] = "fourchette"
--         }
--     },
--     [13] = {
--         [1] = {
--             [1] = "noun",
--             [2] = {
--                 [1] = {
--                     [1] = "Ustensile de table.",
--                     [2] = "12518.0",
--                     [3] = "Des fourchettes, des couteaux et des cuillères ."
--                 },
--                 [2] = {
--                     [1] = "Ecart entre deux valeurs.",
--                     [2] = "12518.1",
--                     [3] = "La fourchette des prix ."
--                 }
--             },
--             [3] = "fourchette"
--         }
--     },
--     [14] = {
--         [1] = {
--             [1] = {
--                 [1] = "La <b>fourchette</b> des prix .",
--                 [5] = 3,
--                 [6] = "12518.1",
--                 ["n"] = 6
--             }
--         }
--     },
--     ["n"] = 14
-- }
