You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

191 lines
4.7 KiB

package translate
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// Azure Translator v3 /translate 官方硬限制:
// - 单次请求总字符数 ≤ 50000(含所有 element, 多目标语言按倍数累加)
// - 单次请求元素(element)个数 ≤ 1000
// - URL 长度 ≤ 2048
// 预留 10% 余量, 避免边界 400
const (
maxCharsPerBatch = 45000
maxTextsPerBatch = 1000
)
type TranslateSys struct {
options Options
client *http.Client
}
func newSys(options Options) (sys *TranslateSys, err error) {
if options.Key == "" {
return nil, fmt.Errorf("microsoft translate: key is required")
}
sys = &TranslateSys{
options: options,
client: &http.Client{Timeout: 30 * time.Second},
}
return
}
type azureInput struct {
Text string `json:"Text"`
}
type azureOutput struct {
DetectedLanguage *struct {
Language string `json:"language"`
} `json:"detectedLanguage"`
Translations []struct {
Text string `json:"text"`
To string `json:"to"`
} `json:"translations"`
}
// TranslateDetect 翻一句。from 为空时不传 from,让微软检测源语种。
func (this *TranslateSys) TranslateDetect(ctx context.Context, from, to, text string) (translated, detected string, err error) {
out, err := this.translateBatch(ctx, azureLang(from), azureLang(to), []string{text})
if err != nil {
return "", "", err
}
if len(out) == 0 {
return "", "", nil
}
return out[0].text, out[0].detected, nil
}
type azureLine struct {
text string
detected string
}
func azureLang(code string) string {
switch strings.ToLower(strings.TrimSpace(code)) {
case "", "auto":
return ""
case "zh", "zh-cn", "zh-hans":
return "zh-Hans"
case "zh-tw", "zh-hk", "zh-hant":
return "zh-Hant"
case "no", "nb", "nn":
return "nb"
default:
return code
}
}
func (this *TranslateSys) Translate(ctx context.Context, from string, to string, texts []string) (results []string, err error) {
results = make([]string, 0, len(texts))
batch := make([]string, 0, maxTextsPerBatch)
batchChars := 0
flush := func() error {
if len(batch) == 0 {
return nil
}
part, e := this.translateBatch(ctx, from, to, batch)
if e != nil {
return e
}
for _, line := range part {
results = append(results, line.text)
}
batch = batch[:0]
batchChars = 0
return nil
}
for _, text := range texts {
tl := len([]rune(text))
// 单条超过单批总字符上限: 独立成批, 仍按一条发送
// (Azure 单条上限同样是 50000, 超大单条的拒绝交给 API 反馈)
if tl >= maxCharsPerBatch {
if err = flush(); err != nil {
return nil, err
}
part, e := this.translateBatch(ctx, from, to, []string{text})
if e != nil {
return nil, e
}
for _, line := range part {
results = append(results, line.text)
}
continue
}
if (batchChars+tl > maxCharsPerBatch || len(batch) >= maxTextsPerBatch) && len(batch) > 0 {
if err = flush(); err != nil {
return nil, err
}
}
batch = append(batch, text)
batchChars += tl
}
if err = flush(); err != nil {
return nil, err
}
return
}
func (this *TranslateSys) translateBatch(ctx context.Context, from, to string, texts []string) ([]azureLine, error) {
q := url.Values{}
q.Set("api-version", "3.0")
if mapped := azureLang(from); mapped != "" {
q.Set("from", mapped)
}
q.Set("to", azureLang(to))
endpoint := this.options.Endpoint + "/translate?" + q.Encode()
inputs := make([]azureInput, 0, len(texts))
for _, t := range texts {
inputs = append(inputs, azureInput{Text: t})
}
body, err := json.Marshal(inputs)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Ocp-Apim-Subscription-Key", this.options.Key)
if this.options.Region != "" {
req.Header.Set("Ocp-Apim-Subscription-Region", this.options.Region)
}
resp, err := this.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("azure translate status=%d body=%s", resp.StatusCode, string(data))
}
var out []azureOutput
if err := json.Unmarshal(data, &out); err != nil {
return nil, fmt.Errorf("azure parse resp: %w body=%s", err, string(data))
}
results := make([]azureLine, 0, len(out))
for _, o := range out {
line := azureLine{}
if o.DetectedLanguage != nil {
line.detected = o.DetectedLanguage.Language
}
if len(o.Translations) > 0 {
line.text = o.Translations[0].Text
}
results = append(results, line)
}
return results, nil
}