// ============================================================================ // translate.go — 工具页翻译 // // 调用 MyMemory 免费接口做中英互译(自动检测方向), // 含每日配额限流提示与错误处理。 // ============================================================================ package main import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "regexp" "strings" "time" ) // TranslateResult 一次翻译的结果,供前端展示。 type TranslateResult struct { Text string `json:"text"` // 翻译结果文本 Detected string `json:"detected"` // 检测到的源语言(zh / en) Target string `json:"target"` // 目标语言(zh / en) } // myMemoryResp MyMemory API 响应(只取需要的字段)。 type myMemoryResp struct { ResponseData struct { TranslatedText string `json:"translatedText"` } `json:"responseData"` ResponseStatus int `json:"responseStatus"` ResponseDetail string `json:"responseDetails"` } // cjkRe 匹配 CJK(中日韩)字符,用于检测文本是否为中文。 // 注意:Go 的 regexp 不支持 \u 转义,需用 \x{...} 形式。 var cjkRe = regexp.MustCompile(`[\x{4e00}-\x{9fff}]`) // detectLang 粗略检测文本语言:含 CJK 字符视为中文,否则视为英文。 // 返回 MyMemory 使用的 langpair 源语言代码。 func detectLang(text string) string { if cjkRe.MatchString(text) { return "zh-CN" } return "en" } // quotaHintRe 匹配 MyMemory 限流提示中的恢复时间(如 "00 HOURS 20 MINUTES 16 SECONDS")。 var quotaHintRe = regexp.MustCompile(`NEXT AVAILABLE IN\s+([\d\sA-Z]+)`) // quotaError 根据 MyMemory 的限流提示生成用户友好的错误信息。 func quotaError(raw string) error { // 从提示语中提取恢复时间,告知用户多久后可用 msg := "翻译服务今日免费额度已用完" if m := quotaHintRe.FindStringSubmatch(raw); len(m) > 1 { parts := strings.Fields(strings.ToUpper(m[1])) // 期望格式:HH HOURS MM MINUTES SS SECONDS if len(parts) >= 6 && parts[1] == "HOURS" && parts[3] == "MINUTES" && parts[5] == "SECONDS" { h, _ := time.ParseDuration(parts[0] + "h") mn, _ := time.ParseDuration(parts[2] + "m") s, _ := time.ParseDuration(parts[4] + "s") wait := h + mn + s if wait > 0 { msg = fmt.Sprintf("翻译服务今日免费额度已用完,约 %s 后恢复", wait.Round(time.Minute)) } } } msg += "(每日限额约 5000 词,请稍后再试)" return errors.New(msg) } // translateMyMemory 调用 MyMemory 免费翻译 API,自动检测中英文方向。 // MyMemory 无 API key,但有每日配额(约 5000 词),超限返回 429。 func translateMyMemory(text string) (TranslateResult, error) { text = strings.TrimSpace(text) if text == "" { return TranslateResult{}, errors.New("请输入要翻译的内容") } src := detectLang(text) target := "en" if src == "en" { target = "zh-CN" } // MyMemory langpair 使用下划线连接:zh-CN|en 或 en|zh-CN endpoint := fmt.Sprintf("https://api.mymemory.translated.net/get?q=%s&langpair=%s", url.QueryEscape(text), url.QueryEscape(src+"|"+target)) client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return TranslateResult{}, err } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36") req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return TranslateResult{}, fmt.Errorf("翻译服务请求失败:%w", err) } defer resp.Body.Close() var mr myMemoryResp if err := json.NewDecoder(resp.Body).Decode(&mr); err != nil { return TranslateResult{}, fmt.Errorf("翻译服务响应解析失败:%w", err) } // MyMemory 超限时 HTTP 仍返回 200,但 responseStatus 为 429 且提示语在 translatedText 中。 if mr.ResponseStatus == 429 { return TranslateResult{}, quotaError(mr.ResponseDetail) } if mr.ResponseStatus != 200 { detail := mr.ResponseDetail if detail == "" { detail = fmt.Sprintf("服务返回状态码 %d", mr.ResponseStatus) } return TranslateResult{}, fmt.Errorf("翻译失败:%s", detail) } if strings.Contains(mr.ResponseData.TranslatedText, "MYMEMORY WARNING") { return TranslateResult{}, quotaError(mr.ResponseData.TranslatedText) } translated := strings.TrimSpace(mr.ResponseData.TranslatedText) if translated == "" { return TranslateResult{}, errors.New("翻译结果为空,请重试") } return TranslateResult{ Text: translated, Detected: src, Target: target, }, nil }