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 }