Blog

研究研究浏览器中的翻译组件

2023-01-15

#Google翻译
#微软翻译
#Yandex翻译
#搜狗翻译
#百度翻译

最近为了给 Twitter Monitor 增加几个翻译源,我研究了几个自带翻译功能的浏览器(不出意外全是 Chromium 系的)

Chrome

首先是老大哥 Chrome,自带的翻译源是 Google 翻译,作为我日常使用的浏览器,我觉得这玩意还是很有用的,处理 Google 翻译的 api 唯一的难点就是那个神秘的 tk ,这个值用于校验传入的内容是否有误,出错了直接送400不解释。

这个 tk 值的原理大概就是根据待翻译文本的每一个字符的 charCode 做计算得出一个的数组,再拿这个数组去做一系列的运算,我还是直接放代码吧

const hl = function (a, b) {
    let c = 0
    for (; c < b.length - 2; c += 3) {
        let d = b.charAt(c + 2)
        d = "a" <= d ? (d.charCodeAt(0) - 87) : Number(d)
        d = "+" == b.charAt(c + 1) ? (a >>> d) : (a << d)
        a = "+" == b.charAt(c) ? (a + d & 4294967295) : (a ^ d)
    }
    return a
}
const getCharCodeList = function (text) {
    let charCodeList = [], charCodeListIndex = 0
    for (let index = 0; index < text.length; index++) {
        let charCode = text.charCodeAt(index)
        if (128 > charCode) {
            charCodeList[charCodeListIndex++] = charCode
        } else {
            if (2048 > charCode) {
                charCodeList[charCodeListIndex++] = charCode >> 6 | 192
            } else  {
                if (55296 == (charCode & 64512) && index + 1 < text.length && 56320 == (text.charCodeAt(index + 1) & 64512)) {
                    charCode = 65536 + ((charCode & 1023) << 10) + (text.charCodeAt(++index) & 1023)
                    charCodeList[charCodeListIndex++] = charCode >> 18 | 240
                    charCodeList[charCodeListIndex++] = charCode >> 12 & 63 | 128
                } else {
                    charCodeList[charCodeListIndex++] = charCode >> 12 | 224
                }
                charCodeList[charCodeListIndex++] = charCode >> 6 & 63 | 128
            }
            charCodeList[charCodeListIndex++] = charCode & 63 | 128
        }
    }
    return charCodeList
}

//https://translate.google.com/translate_a/element.js?cb=gtElInit&hl=zh-CN&client=wt c._ctkk
const GoogleTranslateTk = (originalText = '', tkk = [464385, 3806605782]) => {
    //from https://translate.googleapis.com/_/translate_http/_/js/k=translate_http.tr.zh_CN.D7QeyoDkDhY.O/d=1/exm=el_conf/ed=1/rs=AN8SPfq20C5s1IToiD2r2PKoyh-SRQysPA/m=el_main
    let text
    if (originalText instanceof Array) {
        text = JSON.parse(JSON.stringify(originalText)).join('')
    } else {
        text = originalText
    }
    const charCodeList = getCharCodeList(text)
    let a = tkk[0]
    for (const charCode of charCodeList) {
        a += charCode
        a = hl(a, '+-a^+6')
    }
    a = hl(a, '+-3^+b+-f')
    a ^= tkk[1] ? tkk[1] + 0 : 0
    if (a < 0) {
        a = (a & 2147483647) + 2147483648
    }
    a %= 1E6
    return a.toString() + '.' + (a ^ tkk[0])
}

其中函数 GoogleTranslateTk() 的传入值 tkk 是动态的,每隔一段时间都会更新,不过历史上出现过的任何一个 tkk 计算出来的值都是合法的,所以可以当作是固定的值

至于dom处理还是很简单粗暴的(还是比较好的了),Google 会对链接和特殊文本进行处理,链接会改成类似 <a id=${index}>${content}</a> 的格式,其中这个 index 就表示那段文字中的第几个链接,别的我忘了,后续再补上

Google 翻译太常用了,已经有大量前人研究过这玩意了,剩下的细节方面大家可以看这篇文章

Microsoft Edge

作为 Chromium 系的搅局者,Windows 系统自带的 Edge 那自然是不得不提的存在,Edge 浏览器自带的翻译源是微软翻译,作为前推文翻译的内容提供商(目前是Google),它的翻译质量还算是可以保障的,不过这家其实很简单,只需要获取一个 jwt 就能用了,这个 token 的有效时长不算很长,大约 9.5 分钟

const jwt = await axios.get('https://edge.microsoft.com/translate/auth')

const content = await axios.post(`https://api.cognitive.microsofttranslator.com/translate?from=&to=${target}&api-version=3.0&includeSentenceLength=true`, JSON.stringify(textArray.map(text => ({Text: text}))), {
    headers: {
        'content-type': 'application/json',
        authorization: `Bearer ${jwt}`
    }
})

这家是用奇奇怪怪的 <b${type}>${content}<b${type}> 来分割 dom,其中不同的 tag 会分配给不同的 type,比如 <a> 分配到的是20<strong>分配到的是10

更多的玩法请看参考里面的链接

Yandex Browser

前两家翻译源都支持自动判断语言,但 yandex 翻译需要事先判断语言类型再传出去,通过查看 browser://translate-internals/ 可以得知道 Yandex 浏览器使用 CLD3 检测语言,可以通过 lang_162.js 找到支持的语言

browser://translate-internals/

browser://translate-internals/

我选择使用 github:kwonoj/cld3-asm 判断,仓库里的示例代码已经写得很详细,我就不赘述了

至于 dom 处理跟 Chrome 差不多,直接参考 Chrome 的方案即可

lang = predictFromCLD3()//...

//from yandex browser
const generateSid = () => {
    var t, e, n = Date.now().toString(16)
    for (t = 0, e = 16 - n.length; t < e; t++) {
        n += Math.floor(16 * Math.random()).toString(16)
    }
    return n
}
const supportedLanguageList = ["af","sq","am","ar","hy","az","ba","eu","be","bn","bs","bg","my","ca","ceb","zh","cv","hr","cs","da","nl","sjn","emj","en","eo","et","fi","fr","gl","ka","de","el","gu","ht","he","mrj","hi","hu","is","id","ga","it","ja","jv","kn","kk","kazlat","km","ko","ky","lo","la","lv","lt","lb","mk","mg","ms","ml","mt","mi","mr","mhr","mn","ne","no","pap","fa","pl","pt","pt-BR","pa","ro","ru","gd","sr","si","sk","sl","es","su","sw","sv","tl","tg","ta","tt","te","th","tr","udm","uk","ur","uz","uzbcyr","vi","cy","xh","sah","yi","zu"]

let query = new URLSearchParams({
    translateMode: 'context',
    context_title: 'Twitter Monitor Translator',//自定义的标题,可以自己改
    id: `${generateSid()}-0-0`,
    srv: 'yabrowser',
    lang: `${lang}-${target}`,
    format: 'html',
    options: 2
})
const content = await axios.get('https://browser.translate.yandex.net/api/v1/tr.json/translate?' + query.toString() + '&text=' + (textArray.map(text => encodeURIComponent(text)).join('&text=')))

QQ 浏览器

作为国产浏览器的老大哥之一,QQ 浏览器的使用者还是挺多的,所以我拿它来作为最后一个研究的浏览器。

跟前面三家不一样,QQ 浏览器是用一个搜狗翻译的插件完成这个功能的,这个插件疑似调用了一些 QQ 浏览器的私有 api 或者是过时的 Chromium 的 api,反正我尝试在最新版本的 Chrome (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36) 上加载解压后的拓展程序是用不了的,而 QQ 浏览器没直接在拓展程序页面提供调试插件的办法(难道他们自己都不用?),我懒得研究各种奇技淫巧就直接拆包了

反正没多复杂,很简单就解决了

let body = JSON.stringify({
    from_lang: "auto",
    to_lang: target,
    trans_frag: textArray.map(text => ({text}))
})
const content = axios.post('https://go.ie.sogou.com/qbpc/translate', `S-Param=${body}`)

我研究了一番发现它不会翻译 # ,所以理论上只需要用两个 # 包裹编号的格式就可以解决 dom 的问题了,大概就是 #${index}#

需要注意的是搜狗的 to_lang大小写敏感的,目标语言是中文时必须用 zh-CHS(看到测试没过的时候我是有点慌的)

百度翻译

研究之余我还有一个意外发现,百度翻译不再需要一顿复杂的拿 token 的流程了,鉴于百度 sign 的原理跟 Google 基本是一样的,只不过百度遇到长度大于30的字符串时会切割前中后各10个字符组成总长30的新字符串


const gtk = [320305, 131321201]//应该跟Google差不多,是永久有效的,最好还是实时获取啦

const baiduPrefix = (text) => {
    let textArray = [...text]
    if (textArray.length > 30) {
        return textArray.slice(0, 10).join("") + textArray.slice(Math.floor(textArray.length / 2) - 5, Math.floor(textArray.length / 2) + 5).join("") + textArray.slice(-10).join("")
    }
    return text
}

const sign = GoogleTranslateTk(baiduPrefix(text), gtk)

别的还是自己去抓包啦,反正也不难

DeepL

其实我还是有研究了一番的,不过一直报 429 我就不想研究了,这家风控挺烦人的,我懒得折腾了

2023-02-20 更新

其实这个的原理还是挺好分析的,以前还要逆向客户端,现在看看chrome插件源码就好了,细节方面我不敢说,免得这篇文章被 DeepL 干掉。不过这些细节在代码层面都是直接拍脸上的,所以不难,但不要想当然,所见的未必是原本的意思

碎碎念

  • 腾讯系的翻译网站有三个!三个!我发现这个情况时还是很震撼的,这三个分别是 搜狗翻译腾讯翻译君以及腾讯交互翻译
  • 各家使用的语言代码都有自己的花样,不过一般都比较遵守 ISO 639 的,百度比较特立独行搞了一堆乱七八糟的代码,不加预处理应该是没法跟其他源共用一套来源的
  • 其实我还研究了 Safari,不过实在搞不来 iOS 的抓包外加后面查到有前人提到这个接口一过代理就会访问失败,最后只抓到一个 url: https://sequoia.apple.com
  • 你说我为啥不研究研究360翻译?emmm自己看吧
    我也不用!

    我也不用!


    不过他们好像自己套了一层代理,还挺有意思的
    'https://elephant.browser.360.cn/?t=translate&m=google&anno=3&client=te_lib&format=html&v=1.0&key=AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw&logld=vTE_20200506_00&sl=en&tl=zh-CN&sp=nmt&tc=1&sr=1&tk=669631.848623&mode=1'
    # body我懒得写了,反正就只是套了一层代理,别的格式是一样的
    
  • 我写了两段正则表达式来判断简中还是繁中……虽然感觉很鸡肋,不过写都写了别浪费了
    const isChs = (lang = 'zh') => /^zh(?:_|\-)(?:cn|sg|my|chs)|zh|chs|zho$/.test(lang.toLowerCase())
    const isCht = (lang = 'zh_tw') => /^zh(?:_|\-)(?:tw|hk|mo|cht)|cht$/.test(lang.toLowerCase())
    

参考


评论区