DeepLX/translate/translate.go
2024-11-01 13:13:28 -04:00

279 lines
8.2 KiB
Go

/*
* @Author: Vincent Young
* @Date: 2024-09-16 11:59:24
* @LastEditors: Vincent Yang
* @LastEditTime: 2024-11-01 13:12:25
* @FilePath: /DeepLX/translate/translate.go
* @Telegram: https://t.me/missuo
* @GitHub: https://github.com/missuo
*
* Copyright © 2024 by Vincent, All Rights Reserved.
*/
package translate
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/andybalholm/brotli"
"github.com/tidwall/gjson"
)
const baseURL = "https://www2.deepl.com/jsonrpc"
// makeRequest makes an HTTP request to DeepL API
func makeRequest(postData *PostData, urlMethod string, proxyURL string, dlSession string) (gjson.Result, error) {
urlFull := fmt.Sprintf("%s?client=chrome-extension,1.28.0&method=%s", baseURL, urlMethod)
postStr := formatPostString(postData)
req, err := http.NewRequest("POST", urlFull, bytes.NewReader([]byte(postStr)))
if err != nil {
return gjson.Result{}, err
}
if dlSession != "" {
req.Header = http.Header{
"Accept": []string{"*/*"},
"Accept-Language": []string{"en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5"},
"Authorization": []string{"None"},
"Cache-Control": []string{"no-cache"},
"Content-Type": []string{"application/json"},
"DNT": []string{"1"},
"Origin": []string{"chrome-extension://cofdbpoegempjloogbagkncekinflcnj"},
"Pragma": []string{"no-cache"},
"Priority": []string{"u=1, i"},
"Referer": []string{"https://www.deepl.com/"},
"Sec-Fetch-Dest": []string{"empty"},
"Sec-Fetch-Mode": []string{"cors"},
"Sec-Fetch-Site": []string{"none"},
"Sec-GPC": []string{"1"},
"User-Agent": []string{"DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"},
"Cookie": []string{"dl_session=" + dlSession},
}
} else {
req.Header = http.Header{
"Accept": []string{"*/*"},
"Accept-Language": []string{"en-US,en;q=0.9,zh-CN;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6,zh;q=0.5"},
"Authorization": []string{"None"},
"Cache-Control": []string{"no-cache"},
"Content-Type": []string{"application/json"},
"DNT": []string{"1"},
"Origin": []string{"chrome-extension://cofdbpoegempjloogbagkncekinflcnj"},
"Pragma": []string{"no-cache"},
"Priority": []string{"u=1, i"},
"Referer": []string{"https://www.deepl.com/"},
"Sec-Fetch-Dest": []string{"empty"},
"Sec-Fetch-Mode": []string{"cors"},
"Sec-Fetch-Site": []string{"none"},
"Sec-GPC": []string{"1"},
"User-Agent": []string{"DeepLBrowserExtension/1.28.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"},
}
}
// Setup client with proxy if provided
var client *http.Client
if proxyURL != "" {
proxy, err := url.Parse(proxyURL)
if err != nil {
return gjson.Result{}, err
}
client = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxy)}}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if err != nil {
return gjson.Result{}, err
}
defer resp.Body.Close()
// Check status code before processing response
if resp.StatusCode != http.StatusOK {
switch resp.StatusCode {
case http.StatusTooManyRequests:
return gjson.Result{}, fmt.Errorf("too many requests")
case http.StatusUnauthorized:
return gjson.Result{}, fmt.Errorf("unauthorized")
case http.StatusForbidden:
return gjson.Result{}, fmt.Errorf("forbidden")
default:
return gjson.Result{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
var bodyReader io.Reader
if resp.Header.Get("Content-Encoding") == "br" {
bodyReader = brotli.NewReader(resp.Body)
} else {
bodyReader = resp.Body
}
body, err := io.ReadAll(bodyReader)
if err != nil {
return gjson.Result{}, err
}
return gjson.ParseBytes(body), nil
}
// splitText splits the input text for translation
func splitText(text string, tagHandling bool, proxyURL string, dlSession string) (gjson.Result, error) {
postData := &PostData{
Jsonrpc: "2.0",
Method: "LMT_split_text",
ID: getRandomNumber(),
Params: Params{
CommonJobParams: CommonJobParams{
Mode: "translate",
},
Lang: Lang{
LangUserSelected: "auto",
},
Texts: []string{text},
TextType: map[bool]string{true: "richtext", false: "plaintext"}[tagHandling || isRichText(text)],
},
}
return makeRequest(postData, "LMT_split_text", proxyURL, dlSession)
}
// TranslateByDeepLX performs translation using DeepL API
func TranslateByDeepLX(sourceLang, targetLang, text string, tagHandling string, proxyURL string, dlSession string) (DeepLXTranslationResult, error) {
if text == "" {
return DeepLXTranslationResult{
Code: http.StatusNotFound,
Message: "No text to translate",
}, nil
}
// Split text first
splitResult, err := splitText(text, tagHandling == "html" || tagHandling == "xml", proxyURL, dlSession)
if err != nil {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: err.Error(),
}, nil
}
// Get detected language if source language is auto
if sourceLang == "auto" || sourceLang == "" {
sourceLang = strings.ToLower(splitResult.Get("result.lang.detected").String())
}
// Prepare jobs from split result
var jobs []Job
chunks := splitResult.Get("result.texts.0.chunks").Array()
for idx, chunk := range chunks {
sentence := chunk.Get("sentences.0")
// Handle context
contextBefore := []string{}
contextAfter := []string{}
if idx > 0 {
contextBefore = []string{chunks[idx-1].Get("sentences.0.text").String()}
}
if idx < len(chunks)-1 {
contextAfter = []string{chunks[idx+1].Get("sentences.0.text").String()}
}
jobs = append(jobs, Job{
Kind: "default",
PreferredNumBeams: 4,
RawEnContextBefore: contextBefore,
RawEnContextAfter: contextAfter,
Sentences: []Sentence{{
Prefix: sentence.Get("prefix").String(),
Text: sentence.Get("text").String(),
ID: idx + 1,
}},
})
}
hasRegionalVariant := false
targetLangParts := strings.Split(targetLang, "-")
targetLangCode := targetLangParts[0]
if len(targetLangParts) > 1 {
hasRegionalVariant = true
}
// Prepare translation request
id := getRandomNumber()
postData := &PostData{
Jsonrpc: "2.0",
Method: "LMT_handle_jobs",
ID: id,
Params: Params{
CommonJobParams: CommonJobParams{
Mode: "translate",
RegionalVariant: map[bool]string{true: targetLang, false: ""}[hasRegionalVariant],
},
Lang: Lang{
SourceLangComputed: strings.ToUpper(sourceLang),
TargetLang: strings.ToUpper(targetLangCode),
},
Jobs: jobs,
Priority: 1,
Timestamp: getTimeStamp(getICount(text)),
},
}
// Make translation request
result, err := makeRequest(postData, "LMT_handle_jobs", proxyURL, dlSession)
if err != nil {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: err.Error(),
}, nil
}
// Process translation results
var alternatives []string
var translatedText string
translations := result.Get("result.translations").Array()
if len(translations) > 0 {
// Get alternatives
numBeams := len(translations[0].Get("beams").Array())
for i := 0; i < numBeams; i++ {
var altText string
for _, translation := range translations {
beams := translation.Get("beams").Array()
if i < len(beams) {
altText += beams[i].Get("sentences.0.text").String()
}
}
if altText != "" {
alternatives = append(alternatives, altText)
}
}
// Get main translation
for _, translation := range translations {
translatedText += translation.Get("beams.0.sentences.0.text").String() + " "
}
translatedText = strings.TrimSpace(translatedText)
}
if translatedText == "" {
return DeepLXTranslationResult{
Code: http.StatusServiceUnavailable,
Message: "Translation failed",
}, nil
}
return DeepLXTranslationResult{
Code: http.StatusOK,
ID: id,
Data: translatedText,
Alternatives: alternatives,
SourceLang: sourceLang,
TargetLang: targetLang,
Method: map[bool]string{true: "Pro", false: "Free"}[dlSession != ""],
}, nil
}