| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- // ============================================================================
- // 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
- }
|